Merge remote-tracking branch 'origin/main' into ds-apiserver-schema-builder

This commit is contained in:
Ryan McKinley
2025-12-17 13:42:34 +03:00
638 changed files with 65869 additions and 6629 deletions
+55 -48
View File
@@ -9,19 +9,19 @@ require (
github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.38.0
k8s.io/api v0.34.2
k8s.io/apimachinery v0.34.2
k8s.io/apiserver v0.34.2
k8s.io/client-go v0.34.2
k8s.io/component-base v0.34.2
go.opentelemetry.io/otel v1.39.0
k8s.io/api v0.34.3
k8s.io/apimachinery v0.34.3
k8s.io/apiserver v0.34.3
k8s.io/client-go v0.34.3
k8s.io/component-base v0.34.3
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e
sigs.k8s.io/structured-merge-diff/v6 v6.3.1
)
require (
cel.dev/expr v0.24.0 // indirect
cel.dev/expr v0.25.1 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect
@@ -31,7 +31,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/fatih/color v1.18.0 // indirect
@@ -40,10 +40,20 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
@@ -51,7 +61,7 @@ require (
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.1 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -66,13 +76,11 @@ require (
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jaegertracing/jaeger-idl v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -85,61 +93,60 @@ require (
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.3 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
go.etcd.io/etcd/api/v3 v3.6.6 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect
go.etcd.io/etcd/client/v3 v3.6.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // 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.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
+118 -99
View File
@@ -1,5 +1,5 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
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=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
@@ -27,8 +27,8 @@ github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wX
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -57,19 +57,44 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -89,8 +114,8 @@ 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/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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -133,8 +158,6 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ=
@@ -153,8 +176,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 h1:hQWBtNqRYrI7CWIaUSXXtNKR90KzcUA5uiuxFVWw7sU=
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
@@ -195,8 +216,6 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -208,11 +227,11 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -223,8 +242,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -257,12 +276,12 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ=
go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4=
go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs=
go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI=
go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec=
go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8=
go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA=
go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE=
go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU=
@@ -271,34 +290,34 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
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.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
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.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 h1:oPW/SRFyHgIgxrvNhSBzqvZER2N5kRlci3/rGTOuyWo=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bnmZNO6gBbBta6nohD/1Z+f9waH2oXyBs=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -314,15 +333,15 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -330,18 +349,18 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -353,16 +372,16 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -370,8 +389,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
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=
@@ -382,20 +401,20 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@@ -404,26 +423,26 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE=
k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ=
k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM=
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk=
k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
+56 -8
View File
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) response.Response {
@@ -23,13 +24,13 @@ func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) respons
}
type NotifierPlugin struct {
Type string `json:"type"`
TypeAlias string `json:"typeAlias,omitempty"`
Name string `json:"name"`
Heading string `json:"heading"`
Description string `json:"description"`
Info string `json:"info"`
Options []schema.Field `json:"options"`
Type string `json:"type"`
TypeAlias string `json:"typeAlias,omitempty"`
Name string `json:"name"`
Heading string `json:"heading"`
Description string `json:"description"`
Info string `json:"info"`
Options []Field `json:"options"`
}
result := make([]*NotifierPlugin, 0, len(v2))
@@ -44,9 +45,56 @@ func (hs *HTTPServer) GetAlertNotifiers() func(*contextmodel.ReqContext) respons
Description: s.Description,
Heading: s.Heading,
Info: s.Info,
Options: v1.Options,
Options: schemaFieldsToFields(s.Type, nil, v1.Options),
})
}
return response.JSON(http.StatusOK, result)
}
}
type Field struct {
Element schema.ElementType `json:"element"`
InputType schema.InputType `json:"inputType"`
Label string `json:"label"`
Description string `json:"description"`
Placeholder string `json:"placeholder"`
PropertyName string `json:"propertyName"`
SelectOptions []schema.SelectOption `json:"selectOptions"`
ShowWhen schema.ShowWhen `json:"showWhen"`
Required bool `json:"required"`
Protected bool `json:"protected,omitempty"`
ValidationRule string `json:"validationRule"`
Secure bool `json:"secure"`
DependsOn string `json:"dependsOn"`
SubformOptions []Field `json:"subformOptions"`
}
func schemaFieldsToFields(iType schema.IntegrationType, parent schema.IntegrationFieldPath, fields []schema.Field) []Field {
if fields == nil {
return nil
}
result := make([]Field, 0, len(fields))
for _, f := range fields {
result = append(result, schemaFieldToField(iType, parent, f))
}
return result
}
func schemaFieldToField(iType schema.IntegrationType, parent schema.IntegrationFieldPath, f schema.Field) Field {
return Field{
Element: f.Element,
InputType: f.InputType,
Label: f.Label,
Description: f.Description,
Placeholder: f.Placeholder,
PropertyName: f.PropertyName,
SelectOptions: f.SelectOptions,
ShowWhen: f.ShowWhen,
Required: f.Required,
ValidationRule: f.ValidationRule,
Secure: f.Secure,
DependsOn: f.DependsOn,
SubformOptions: schemaFieldsToFields(iType, append(parent, f.PropertyName), f.SubformOptions),
Protected: models.IsProtectedField(iType, append(parent, f.PropertyName)),
}
}
+3 -1
View File
@@ -493,7 +493,9 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
// swagger:route GET /dashboards/home dashboards getHomeDashboard
//
// Get home dashboard.
// NOTE: the home dashboard is configured in preferences. This API will be removed in G13
//
// Deprecated: true
//
// Responses:
// 200: getHomeDashboardResponse
+4 -6
View File
@@ -112,17 +112,15 @@ func TestGetHomeDashboard(t *testing.T) {
}
func newTestLive(t *testing.T) *live.GrafanaLive {
features := featuremgmt.WithFeatures()
cfg := setting.NewCfg()
cfg.AppURL = "http://localhost:3000/"
gLive, err := live.ProvideService(nil, cfg,
gLive, err := live.ProvideService(cfg,
routing.NewRouteRegister(),
nil, nil, nil, nil,
nil,
&usagestats.UsageStatsMock{T: t},
features, acimpl.ProvideAccessControl(features),
&dashboards.FakeDashboardService{},
nil, nil)
featuremgmt.WithFeatures(),
&dashboards.FakeDashboardService{}, nil)
require.NoError(t, err)
return gLive
}
+1 -1
View File
@@ -638,7 +638,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.web
m.Use(requestmeta.SetupRequestMetadata())
m.Use(middleware.RequestTracing(hs.tracer, middleware.SkipTracingPaths))
m.Use(middleware.RequestTracing(hs.tracer, middleware.ShouldTraceWithExceptions))
m.Use(middleware.RequestMetrics(hs.Features, hs.Cfg, hs.promRegister))
m.UseMiddleware(hs.LoggerMiddleware.Middleware())
+1 -3
View File
@@ -294,6 +294,7 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(c *contextmodel.ReqContext) respo
}
func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) {
query.ExcludeHiddenUsers = true
result, err := hs.orgService.SearchOrgUsers(c.Req.Context(), query)
if err != nil {
return nil, err
@@ -303,9 +304,6 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or
userIDs := map[string]bool{}
authLabelsUserIDs := make([]int64, 0, len(result.OrgUsers))
for _, user := range result.OrgUsers {
if dtos.IsHiddenUser(user.Login, c.SignedInUser, hs.Cfg) {
continue
}
user.AvatarURL = dtos.GetGravatarUrl(hs.Cfg, user.Email)
userIDs[fmt.Sprint(user.UserID)] = true
+18 -1
View File
@@ -171,11 +171,16 @@ func TestIntegrationOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{Login: testUserLogin, Email: "testUser@grafana.com"},
{Login: "user1", Email: "user1@grafana.com"},
{Login: "user2", Email: "user2@grafana.com"},
},
}
orgService.SearchOrgUsersFn = func(ctx context.Context, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) {
require.True(t, query.ExcludeHiddenUsers)
return orgService.ExpectedSearchOrgUsersResult, nil
}
defer func() { orgService.SearchOrgUsersFn = nil }()
sc.handlerFunc = hs.GetOrgUsersForCurrentOrg
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
@@ -191,6 +196,18 @@ func TestIntegrationOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) {
loggedInUserScenarioWithRole(t, "When calling GET as an admin on", "GET", "api/org/users/lookup",
"api/org/users/lookup", org.RoleAdmin, func(sc *scenarioContext) {
orgService.ExpectedSearchOrgUsersResult = &org.SearchOrgUsersQueryResult{
OrgUsers: []*org.OrgUserDTO{
{Login: testUserLogin, Email: "testUser@grafana.com"},
{Login: "user2", Email: "user2@grafana.com"},
},
}
orgService.SearchOrgUsersFn = func(ctx context.Context, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) {
require.True(t, query.ExcludeHiddenUsers)
return orgService.ExpectedSearchOrgUsersResult, nil
}
defer func() { orgService.SearchOrgUsersFn = nil }()
sc.handlerFunc = hs.GetOrgUsersForCurrentOrgLookup
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+31 -21
View File
@@ -8,28 +8,37 @@ require (
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v3 v3.0.4
k8s.io/apimachinery v0.34.2
k8s.io/apiserver v0.34.2
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
k8s.io/apimachinery v0.34.3
k8s.io/apiserver v0.34.3
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
@@ -38,21 +47,22 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
)
+70 -46
View File
@@ -1,3 +1,5 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -13,20 +15,46 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -38,8 +66,6 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -48,8 +74,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -79,16 +103,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -102,22 +126,22 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -128,12 +152,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -141,18 +165,18 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE=
k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
+1
View File
@@ -162,6 +162,7 @@ var serviceIdentityTokenPermissions = []string{
"collections.grafana.app:*", // user stars
"plugins.grafana.app:*",
"historian.alerting.grafana.app:*",
"advisor.grafana.app:*",
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
"secret.grafana.app/securevalues:decrypt",
+9 -2
View File
@@ -19,11 +19,18 @@ func (NoopBackend) Shutdown() {}
func (NoopBackend) String() string { return "" }
// NoopPolicyRuleProvider is a no-op implementation of PolicyRuleProvider
type NoopPolicyRuleProvider struct{}
func ProvideNoopPolicyRuleProvider() PolicyRuleProvider { return &NoopPolicyRuleProvider{} }
func (NoopPolicyRuleProvider) PolicyRuleProvider(PolicyRuleEvaluators) audit.PolicyRuleEvaluator {
return NoopPolicyRuleEvaluator{}
}
// NoopPolicyRuleEvaluator is a no-op implementation of audit.PolicyRuleEvaluator
type NoopPolicyRuleEvaluator struct{}
func ProvideNoopPolicyRuleEvaluator() audit.PolicyRuleEvaluator { return &NoopPolicyRuleEvaluator{} }
func (NoopPolicyRuleEvaluator) EvaluatePolicyRule(authorizer.Attributes) audit.RequestAuditConfig {
return audit.RequestAuditConfig{Level: auditinternal.LevelNone}
}
+59
View File
@@ -0,0 +1,59 @@
package auditing
import (
"slices"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"k8s.io/apimachinery/pkg/runtime/schema"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// PolicyRuleEvaluators is a map of API group+version to audit.PolicyRuleEvaluator
type PolicyRuleEvaluators = map[schema.GroupVersion]audit.PolicyRuleEvaluator
type PolicyRuleProvider interface {
PolicyRuleProvider(evaluators PolicyRuleEvaluators) audit.PolicyRuleEvaluator
}
// PolicyRuleEvaluator alias for easier imports.
type PolicyRuleEvaluator = audit.PolicyRuleEvaluator
// DefaultGrafanaPolicyRuleEvaluator provides a sane default configuration for audit logging for API group+versions.
type defaultGrafanaPolicyRuleEvaluator struct{}
var _ PolicyRuleEvaluator = &defaultGrafanaPolicyRuleEvaluator{}
func NewDefaultGrafanaPolicyRuleEvaluator() audit.PolicyRuleEvaluator {
return defaultGrafanaPolicyRuleEvaluator{}
}
func (defaultGrafanaPolicyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Attributes) audit.RequestAuditConfig {
// Skip non-resource and watch requests otherwise it is too noisy.
if !attrs.IsResourceRequest() || attrs.GetVerb() == utils.VerbWatch {
return audit.RequestAuditConfig{
Level: auditinternal.LevelNone,
}
}
// Skip auditing if the user is part of the privileged group.
// The loopback client uses this group, so requests initiated in `/api/` would be duplicated.
if u := attrs.GetUser(); u != nil && slices.Contains(u.GetGroups(), user.SystemPrivilegedGroup) {
return audit.RequestAuditConfig{
Level: auditinternal.LevelNone,
}
}
return audit.RequestAuditConfig{
Level: auditinternal.LevelMetadata,
OmitStages: []auditinternal.Stage{
// Only log on StageResponseComplete
auditinternal.StageRequestReceived,
auditinternal.StageResponseStarted,
auditinternal.StagePanic,
},
OmitManagedFields: false, // Setting it to true causes extra copying/unmarshalling.
}
}
+73
View File
@@ -0,0 +1,73 @@
package auditing_test
import (
"testing"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apiserver/auditing"
"github.com/stretchr/testify/require"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
func TestDefaultGrafanaPolicyRuleEvaluator(t *testing.T) {
t.Parallel()
evaluator := auditing.NewDefaultGrafanaPolicyRuleEvaluator()
require.NotNil(t, evaluator)
t.Run("returns audit level none for non-resource requests", func(t *testing.T) {
t.Parallel()
attrs := authorizer.AttributesRecord{
ResourceRequest: false,
}
config := evaluator.EvaluatePolicyRule(attrs)
require.Equal(t, auditinternal.LevelNone, config.Level)
})
t.Run("returns audit level none for watch requests", func(t *testing.T) {
t.Parallel()
attrs := authorizer.AttributesRecord{
ResourceRequest: true,
Verb: utils.VerbWatch,
}
config := evaluator.EvaluatePolicyRule(attrs)
require.Equal(t, auditinternal.LevelNone, config.Level)
})
t.Run("returns audit level none for requests from privileged group", func(t *testing.T) {
t.Parallel()
attrs := authorizer.AttributesRecord{
ResourceRequest: true,
Verb: utils.VerbCreate,
User: &user.DefaultInfo{
Groups: []string{"test-group", user.SystemPrivilegedGroup},
},
}
config := evaluator.EvaluatePolicyRule(attrs)
require.Equal(t, auditinternal.LevelNone, config.Level)
})
t.Run("return audit level metadata for other resource requests", func(t *testing.T) {
t.Parallel()
attrs := authorizer.AttributesRecord{
ResourceRequest: true,
Verb: utils.VerbCreate,
User: &user.DefaultInfo{
Name: "test-user",
Groups: []string{"test-group"},
},
}
config := evaluator.EvaluatePolicyRule(attrs)
require.Equal(t, auditinternal.LevelMetadata, config.Level)
})
}
+50 -44
View File
@@ -5,18 +5,18 @@ go 1.25.5
require (
github.com/google/go-cmp v0.7.0
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
github.com/grafana/grafana-app-sdk/logging v0.48.3
github.com/grafana/grafana-app-sdk/logging v0.48.7
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
k8s.io/apimachinery v0.34.2
k8s.io/apiserver v0.34.2
k8s.io/component-base v0.34.2
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
k8s.io/apimachinery v0.34.3
k8s.io/apiserver v0.34.3
k8s.io/component-base v0.34.3
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/structured-merge-diff/v6 v6.3.1
)
@@ -26,7 +26,7 @@ require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -34,14 +34,24 @@ require (
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
@@ -50,60 +60,56 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.3 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
go.etcd.io/etcd/api/v3 v3.6.6 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect
go.etcd.io/etcd/client/v3 v3.6.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.2 // indirect
k8s.io/client-go v0.34.2 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
k8s.io/api v0.34.3 // indirect
k8s.io/client-go v0.34.3 // indirect
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
+110 -89
View File
@@ -11,8 +11,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -32,17 +32,42 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@@ -54,8 +79,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -71,8 +96,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
@@ -85,8 +110,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLW
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -99,8 +122,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
@@ -129,11 +150,11 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -158,12 +179,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ=
go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4=
go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs=
go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI=
go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec=
go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8=
go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA=
go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE=
go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU=
@@ -172,26 +193,26 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
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/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/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
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/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -207,8 +228,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -219,30 +240,30 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -250,8 +271,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
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=
@@ -260,20 +281,20 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@@ -281,26 +302,26 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE=
k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ=
k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM=
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo=
k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk=
k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
+16 -15
View File
@@ -10,14 +10,14 @@ replace github.com/docker/docker => github.com/moby/moby v28.0.1+incompatible
require (
github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group
github.com/urfave/cli/v2 v2.27.7 // @grafana/grafana-backend-group
go.opentelemetry.io/otel v1.38.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/sdk v1.38.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/trace v1.38.0 // indirect; @grafana/grafana-backend-group
golang.org/x/net v0.47.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
golang.org/x/sync v0.18.0 // @grafana/alerting-backend
golang.org/x/text v0.31.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel v1.39.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/sdk v1.39.0 // indirect; @grafana/grafana-backend-group
go.opentelemetry.io/otel/trace v1.39.0 // indirect; @grafana/grafana-backend-group
golang.org/x/net v0.48.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
golang.org/x/sync v0.19.0 // @grafana/alerting-backend
golang.org/x/text v0.32.0 // indirect; @grafana/grafana-backend-group
google.golang.org/grpc v1.77.0 // indirect; @grafana/plugins-platform-backend
google.golang.org/protobuf v1.36.10 // indirect; @grafana/plugins-platform-backend
google.golang.org/protobuf v1.36.11 // indirect; @grafana/plugins-platform-backend
)
require (
@@ -28,10 +28,10 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
golang.org/x/sys v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
golang.org/x/sys v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
)
require (
@@ -46,6 +46,7 @@ require (
github.com/Khan/genqlient v0.8.1 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
@@ -56,12 +57,12 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.12.2 // indirect
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
)
+32 -30
View File
@@ -12,6 +12,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -61,8 +63,8 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs=
@@ -71,48 +73,48 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9
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.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
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/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0=
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0=
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+3 -3
View File
@@ -6,10 +6,10 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/subcommands v1.2.0
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
golang.org/x/tools v0.39.0
golang.org/x/tools v0.40.0
)
require (
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
)
+6 -6
View File
@@ -4,9 +4,9 @@ github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
+14 -12
View File
@@ -9,18 +9,18 @@ require (
github.com/grafana/cog v0.0.46
github.com/grafana/cuetsy v0.1.11
github.com/matryer/is v1.4.1
golang.org/x/tools v0.39.0
golang.org/x/tools v0.40.0
)
require (
github.com/cockroachdb/apd/v2 v2.0.2 // indirect
github.com/dave/jennifer v1.7.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/proto v1.13.2 // indirect
github.com/expr-lang/expr v1.17.6 // indirect
github.com/emicklei/proto v1.14.2 // indirect
github.com/expr-lang/expr v1.17.7 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-test/deep v1.1.1 // indirect
github.com/golang/glog v1.2.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
@@ -29,9 +29,10 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
@@ -39,18 +40,19 @@ require (
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yalue/merged_fs v1.3.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+27 -22
View File
@@ -9,16 +9,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY=
github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I=
github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -47,6 +49,7 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -57,8 +60,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
@@ -73,6 +76,7 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -81,6 +85,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
@@ -94,22 +99,22 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
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/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+1 -1
View File
@@ -224,7 +224,7 @@ var (
MStatTotalRepositories prometheus.Gauge
// MUnifiedStorageMigrationStatus indicates the migration status for unified storage in this instance.
// Possible values: 0 (default/undefined), 1 (migration disabled), 2 (migration would run).
// Possible values: 0 (default/undefined), 1 (migration disabled), 2 (migration would run), 3 (migration will run).
MUnifiedStorageMigrationStatus prometheus.Gauge
)
+3
View File
@@ -204,6 +204,9 @@ type Panel struct {
Options map[string]any `json:"options,omitempty"`
// Field options allow you to change how the data is displayed in your visualizations.
FieldConfig *FieldConfigSource `json:"fieldConfig,omitempty"`
// When a panel is migrated from a previous version (Angular to React), this field is set to the original panel type.
// This is used to determine the original panel type when migrating to a new version so the plugin migration can be applied.
AutoMigrateFrom *string `json:"autoMigrateFrom,omitempty"`
}
// NewPanel creates a new Panel object.
+10 -6
View File
@@ -73,16 +73,20 @@ func RouteOperationName(req *http.Request) (string, bool) {
return "", false
}
// Paths that don't need tracing spans applied to them because of the
// little value that would provide us
func SkipTracingPaths(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/public/") ||
func ShouldTraceWithExceptions(req *http.Request) bool {
// Paths that don't need tracing spans applied to them because of the
// little value that would provide us
if strings.HasPrefix(req.URL.Path, "/public/") ||
req.URL.Path == "/robots.txt" ||
req.URL.Path == "/favicon.ico" ||
req.URL.Path == "/api/health"
req.URL.Path == "/api/health" {
return false
}
return true
}
func TraceAllPaths(req *http.Request) bool {
func ShouldTraceAllPaths(req *http.Request) bool {
return true
}
+13 -12
View File
@@ -16,11 +16,11 @@ require (
cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/dave/dst v0.27.3 // indirect
github.com/emicklei/proto v1.13.2 // indirect
github.com/expr-lang/expr v1.17.6 // indirect
github.com/emicklei/proto v1.14.2 // indirect
github.com/expr-lang/expr v1.17.7 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -28,7 +28,7 @@ require (
github.com/huandu/xstrings v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
@@ -37,17 +37,18 @@ require (
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yalue/merged_fs v1.3.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+30 -26
View File
@@ -10,16 +10,18 @@ github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEy
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY=
github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/emicklei/proto v1.14.2 h1:wJPxPy2Xifja9cEMrcA/g08art5+7CGJNFNk35iXC1I=
github.com/emicklei/proto v1.14.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
@@ -51,8 +53,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
@@ -76,8 +78,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA=
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b h1:fPVI9E6QNFYI0Ph3XpKUDrcAvbCifHvqYJcntFLPog8=
github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
@@ -88,26 +90,28 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+43 -33
View File
@@ -8,13 +8,13 @@ require (
github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134
github.com/json-iterator/go v1.1.12
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.3
github.com/prometheus/common v0.67.4
github.com/prometheus/prometheus v0.303.1
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
google.golang.org/protobuf v1.36.10
k8s.io/apimachinery v0.34.2
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
google.golang.org/protobuf v1.36.11
k8s.io/apimachinery v0.34.3
)
require (
@@ -36,16 +36,26 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
@@ -61,11 +71,10 @@ require (
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jaegertracing/jaeger-idl v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -80,47 +89,48 @@ require (
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // 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.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.242.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/client-go v0.34.2 // indirect
k8s.io/client-go v0.34.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
)
+96 -88
View File
@@ -66,36 +66,46 @@ github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7Bd
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0=
github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=
github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync=
github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ=
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU=
github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
@@ -114,8 +124,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
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/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -162,8 +172,6 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -184,8 +192,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -239,10 +247,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY=
github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0nRD9XCjaOt1lsYck=
github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8=
@@ -280,34 +288,34 @@ go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFX
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
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.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
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.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc=
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 h1:oPW/SRFyHgIgxrvNhSBzqvZER2N5kRlci3/rGTOuyWo=
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bnmZNO6gBbBta6nohD/1Z+f9waH2oXyBs=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
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 v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
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.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -321,27 +329,27 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -352,22 +360,22 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
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=
@@ -378,14 +386,14 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -396,18 +404,18 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
@@ -8,6 +8,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/apimachinery/utils"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
@@ -33,8 +34,11 @@ func (d dashboardStorageWrapper) Update(ctx context.Context, name string, objInf
obj, created, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
if err == nil && ns.OrgID > 0 && d.live != nil {
if err := d.live.DashboardSaved(ns.OrgID, name); err != nil {
logging.FromContext(ctx).Info("live dashboard update failed", "err", err)
m, err := utils.MetaAccessor(obj)
if err == nil {
if err := d.live.DashboardSaved(ns.OrgID, name, m.GetResourceVersion()); err != nil {
logging.FromContext(ctx).Info("live dashboard update failed", "err", err)
}
}
}
return obj, created, err
@@ -154,12 +154,6 @@ func (a *dashboardSqlAccess) executeQuery(ctx context.Context, helper *legacysql
return nil
})
// Use transaction from unified storage if available in the context.
// This allows us to run migrations in a transaction which is specifically required for SQLite.
if tx == nil {
tx = resource.TransactionFromContext(ctx)
}
if tx != nil {
return tx.QueryContext(ctx, query, args...)
}
+2 -1
View File
@@ -222,7 +222,7 @@ func RegisterAPIService(
return builder
}
func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceIndexProvider, libraryElementProvider schemaversion.LibraryElementIndexProvider, resourcePermissionsSvc *dynamic.NamespaceableResourceInterface) *DashboardsAPIBuilder {
func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceIndexProvider, libraryElementProvider schemaversion.LibraryElementIndexProvider, resourcePermissionsSvc *dynamic.NamespaceableResourceInterface, search *SearchHandler) *DashboardsAPIBuilder {
migration.Initialize(datasourceProvider, libraryElementProvider, migration.DefaultCacheTTL)
return &DashboardsAPIBuilder{
minRefreshInterval: "10s",
@@ -231,6 +231,7 @@ func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles,
dashboardService: &dashsvc.DashboardServiceImpl{}, // for validation helpers only
folderClientProvider: folderClientProvider,
resourcePermissionsSvc: resourcePermissionsSvc,
search: search,
isStandalone: true,
}
}
@@ -40,7 +40,7 @@ func NewResourcePermissionsAuthorizer(
return &ResourcePermissionsAuthorizer{
accessClient: accessClient,
parentProvider: parentProvider,
logger: log.New("iam.resource-permissions-authorizer"),
logger: log.New("iam.authorizer.resource-permissions"),
}
}
@@ -216,8 +216,7 @@ func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list run
// Skip item on error fetching parent
r.logger.Warn("filter list: error fetching parent, skipping item",
"error", err.Error(),
"namespace",
item.Namespace,
"namespace", item.Namespace,
"group", target.ApiGroup,
"resource", target.Resource,
"name", target.Name,
+4 -2
View File
@@ -21,6 +21,7 @@ import (
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
"github.com/grafana/authlib/authn"
"github.com/grafana/authlib/types"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
@@ -142,6 +143,8 @@ func NewAPIService(
features featuremgmt.FeatureToggles,
zClient zanzana.Client,
reg prometheus.Registerer,
tokenExchanger authn.TokenExchanger,
authorizerDialConfigs map[schema.GroupResource]iamauthorizer.DialConfig,
) *IdentityAccessManagementAPIBuilder {
store := legacy.NewLegacySQLStores(dbProvider)
resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider)
@@ -150,9 +153,8 @@ func NewAPIService(
resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
coreRoleAuthorizer := iamauthorizer.NewCoreRoleAuthorizer(accessClient)
// TODO: in a follow up PR, make this configurable
resourceParentProvider := iamauthorizer.NewApiParentProvider(
iamauthorizer.NewRemoteConfigProvider(map[schema.GroupResource]iamauthorizer.DialConfig{}, nil),
iamauthorizer.NewRemoteConfigProvider(authorizerDialConfigs, tokenExchanger),
iamauthorizer.Versions,
)
@@ -39,6 +39,10 @@ func (m mockProvisioningV0alpha1Interface) Jobs(namespace string) client.JobInte
panic("not needed for testing")
}
func (m mockProvisioningV0alpha1Interface) Connections(namespace string) client.ConnectionInterface {
panic("not needed for testing")
}
func (m mockProvisioningV0alpha1Interface) Repositories(namespace string) client.RepositoryInterface {
if m.repositoriesFunc != nil {
return m.repositoriesFunc(namespace)
@@ -154,9 +154,12 @@ func TestJobProgressRecorderWarningStatus(t *testing.T) {
// Verify the final status includes warnings
require.NotNil(t, finalStatus.Warnings)
assert.Len(t, finalStatus.Warnings, 3)
assert.Contains(t, finalStatus.Warnings[0], "deprecated API used")
assert.Contains(t, finalStatus.Warnings[1], "missing optional field")
assert.Contains(t, finalStatus.Warnings[2], "validation warning")
expectedWarnings := []string{
"deprecated API used (file: dashboards/test.json, name: test-resource, action: updated)",
"missing optional field (file: dashboards/test2.json, name: test-resource-2, action: created)",
"validation warning (file: datasources/test.yaml, name: test-resource-3, action: created)",
}
assert.ElementsMatch(t, finalStatus.Warnings, expectedWarnings)
// Verify the state is set to Warning
assert.Equal(t, provisioning.JobStateWarning, finalStatus.State)
+159 -82
View File
@@ -31,6 +31,7 @@ import (
dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
connectionvalidation "github.com/grafana/grafana/apps/provisioning/pkg/connection"
appcontroller "github.com/grafana/grafana/apps/provisioning/pkg/controller"
clientset "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned"
client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
@@ -328,91 +329,148 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.DecisionDeny, "failed to find requester", err
}
// Different routes may need different permissions.
// * Reading and modifying a repository's configuration requires administrator privileges.
// * Reading a repository's limited configuration (/stats & /settings) requires viewer privileges.
// * Reading a repository's files requires viewer privileges.
// * Reading a repository's refs requires viewer privileges.
// * Editing a repository's files requires editor privileges.
// * Syncing a repository requires editor privileges.
// * Exporting a repository requires administrator privileges.
// * Migrating a repository requires administrator privileges.
// * Testing a repository configuration requires administrator privileges.
// * Viewing a repository's history requires editor privileges.
switch a.GetResource() {
case provisioning.RepositoryResourceInfo.GetName():
// TODO: Support more fine-grained permissions than the basic roles. Especially on Enterprise.
switch a.GetSubresource() {
case "", "test", "jobs":
// Doing something with the repository itself.
if id.GetOrgRole().Includes(identity.RoleAdmin) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "admin role is required", nil
case "refs":
// This is strictly a read operation. It is handy on the frontend for viewers.
if id.GetOrgRole().Includes(identity.RoleViewer) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "viewer role is required", nil
case "files":
// Access to files is controlled by the AccessClient
return authorizer.DecisionAllow, "", nil
case "resources", "sync", "history":
// These are strictly read operations.
// Sync can also be somewhat destructive, but it's expected to be fine to import changes.
if id.GetOrgRole().Includes(identity.RoleEditor) {
return authorizer.DecisionAllow, "", nil
} else {
return authorizer.DecisionDeny, "editor role is required", nil
}
case "status":
if id.GetOrgRole().Includes(identity.RoleViewer) && a.GetVerb() == apiutils.VerbGet {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "users cannot update the status of a repository", nil
default:
if id.GetIsGrafanaAdmin() {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "unmapped subresource defaults to no access", nil
}
case "stats":
// This can leak information one shouldn't necessarily have access to.
if id.GetOrgRole().Includes(identity.RoleAdmin) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "admin role is required", nil
case "settings":
// This is strictly a read operation. It is handy on the frontend for viewers.
if id.GetOrgRole().Includes(identity.RoleViewer) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "viewer role is required", nil
case provisioning.JobResourceInfo.GetName(),
provisioning.HistoricJobResourceInfo.GetName():
// Jobs are shown on the configuration page.
if id.GetOrgRole().Includes(identity.RoleAdmin) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "admin role is required", nil
default:
// We haven't bothered with this kind yet.
if id.GetIsGrafanaAdmin() {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "unmapped kind defaults to no access", nil
}
return b.authorizeResource(ctx, a, id)
})
}
// authorizeResource handles authorization for different resources.
// Different routes may need different permissions.
// * Reading and modifying a repository's configuration requires administrator privileges.
// * Reading a repository's limited configuration (/stats & /settings) requires viewer privileges.
// * Reading a repository's files requires viewer privileges.
// * Reading a repository's refs requires viewer privileges.
// * Editing a repository's files requires editor privileges.
// * Syncing a repository requires editor privileges.
// * Exporting a repository requires administrator privileges.
// * Migrating a repository requires administrator privileges.
// * Testing a repository configuration requires administrator privileges.
// * Viewing a repository's history requires editor privileges.
func (b *APIBuilder) authorizeResource(ctx context.Context, a authorizer.Attributes, id identity.Requester) (authorizer.Decision, string, error) {
switch a.GetResource() {
case provisioning.RepositoryResourceInfo.GetName():
return b.authorizeRepositorySubresource(a, id)
case "stats":
return b.authorizeStats(id)
case "settings":
return b.authorizeSettings(id)
case provisioning.JobResourceInfo.GetName(), provisioning.HistoricJobResourceInfo.GetName():
return b.authorizeJobs(id)
case provisioning.ConnectionResourceInfo.GetName():
return b.authorizeConnectionSubresource(a, id)
default:
return b.authorizeDefault(id)
}
}
// authorizeRepositorySubresource handles authorization for repository subresources.
func (b *APIBuilder) authorizeRepositorySubresource(a authorizer.Attributes, id identity.Requester) (authorizer.Decision, string, error) {
// TODO: Support more fine-grained permissions than the basic roles. Especially on Enterprise.
switch a.GetSubresource() {
case "", "test":
// Doing something with the repository itself.
if id.GetOrgRole().Includes(identity.RoleAdmin) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "admin role is required", nil
case "jobs":
// Posting jobs requires editor privileges (for syncing).
if id.GetOrgRole().Includes(identity.RoleAdmin) || id.GetOrgRole().Includes(identity.RoleEditor) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "editor role is required", nil
case "refs":
// This is strictly a read operation. It is handy on the frontend for viewers.
if id.GetOrgRole().Includes(identity.RoleViewer) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "viewer role is required", nil
case "files":
// Access to files is controlled by the AccessClient
return authorizer.DecisionAllow, "", nil
case "resources", "sync", "history":
// These are strictly read operations.
// Sync can also be somewhat destructive, but it's expected to be fine to import changes.
if id.GetOrgRole().Includes(identity.RoleEditor) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "editor role is required", nil
case "status":
if id.GetOrgRole().Includes(identity.RoleViewer) && a.GetVerb() == apiutils.VerbGet {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "users cannot update the status of a repository", nil
default:
if id.GetIsGrafanaAdmin() {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "unmapped subresource defaults to no access", nil
}
}
// authorizeStats handles authorization for stats resource.
func (b *APIBuilder) authorizeStats(id identity.Requester) (authorizer.Decision, string, error) {
// This can leak information one shouldn't necessarily have access to.
if id.GetOrgRole().Includes(identity.RoleAdmin) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "admin role is required", nil
}
// authorizeSettings handles authorization for settings resource.
func (b *APIBuilder) authorizeSettings(id identity.Requester) (authorizer.Decision, string, error) {
// This is strictly a read operation. It is handy on the frontend for viewers.
if id.GetOrgRole().Includes(identity.RoleViewer) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "viewer role is required", nil
}
// authorizeJobs handles authorization for job resources.
func (b *APIBuilder) authorizeJobs(id identity.Requester) (authorizer.Decision, string, error) {
// Jobs are shown on the configuration page.
if id.GetOrgRole().Includes(identity.RoleAdmin) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "admin role is required", nil
}
// authorizeRepositorySubresource handles authorization for connections subresources.
func (b *APIBuilder) authorizeConnectionSubresource(a authorizer.Attributes, id identity.Requester) (authorizer.Decision, string, error) {
switch a.GetSubresource() {
case "":
// Doing something with the connection itself.
if id.GetOrgRole().Includes(identity.RoleAdmin) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "admin role is required", nil
case "status":
if id.GetOrgRole().Includes(identity.RoleViewer) && a.GetVerb() == apiutils.VerbGet {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "users cannot update the status of a connection", nil
default:
if id.GetIsGrafanaAdmin() {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "unmapped subresource defaults to no access", nil
}
}
// authorizeDefault handles authorization for unmapped resources.
func (b *APIBuilder) authorizeDefault(id identity.Requester) (authorizer.Decision, string, error) {
// We haven't bothered with this kind yet.
if id.GetIsGrafanaAdmin() {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "unmapped kind defaults to no access", nil
}
func (b *APIBuilder) GetGroupVersion() schema.GroupVersion {
return provisioning.SchemeGroupVersion
}
@@ -487,10 +545,19 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
storage[provisioning.HistoricJobResourceInfo.StoragePath()] = historicJobStore
}
connectionsStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, provisioning.ConnectionResourceInfo, opts.OptsGetter)
if err != nil {
return fmt.Errorf("failed to create connection storage: %w", err)
}
connectionStatusStorage := grafanaregistry.NewRegistryStatusStore(opts.Scheme, connectionsStore)
storage[provisioning.JobResourceInfo.StoragePath()] = jobStore
storage[provisioning.RepositoryResourceInfo.StoragePath()] = repositoryStorage
storage[provisioning.RepositoryResourceInfo.StoragePath("status")] = repositoryStatusStorage
storage[provisioning.ConnectionResourceInfo.StoragePath()] = connectionsStore
storage[provisioning.ConnectionResourceInfo.StoragePath("status")] = connectionStatusStorage
// TODO: Add some logic so that the connectors can registered themselves and we don't have logic all over the place
storage[provisioning.RepositoryResourceInfo.StoragePath("test")] = NewTestConnector(b, repository.NewRepositoryTesterWithExistingChecker(repository.NewSimpleRepositoryTester(b.validator), b.VerifyAgainstExistingRepositories))
storage[provisioning.RepositoryResourceInfo.StoragePath("files")] = NewFilesConnector(b, b.parsers, b.clients, b.access)
@@ -533,6 +600,11 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis
if ok {
return nil
}
// TODO: complete this as part of https://github.com/grafana/git-ui-sync-project/issues/700
c, ok := obj.(*provisioning.Connection)
if ok {
return connectionvalidation.MutateConnection(c)
}
r, ok := obj.(*provisioning.Repository)
if !ok {
@@ -582,6 +654,11 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
return nil
}
connection, ok := obj.(*provisioning.Connection)
if ok {
return connectionvalidation.ValidateConnection(connection)
}
// Validate Jobs
job, ok := obj.(*provisioning.Job)
if ok {
@@ -3,6 +3,7 @@ package resources
import (
"context"
"fmt"
"net/http"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -276,6 +277,16 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D
// FIXME: to make sure if behaves in the same way as in sync, we should
// we should refactor the code to use the same function.
if r.shouldUpdateGrafanaDB(opts, parsed) {
// HACK: Get the has from repository -- this will avoid an additional RV increment
// we should change the signature of Create and Update to return FileInfo instead
info, _ = r.repo.Read(ctx, opts.Path, opts.Ref)
if info != nil {
parsed.Meta.SetSourceProperties(utils.SourceProperties{
Path: opts.Path,
Checksum: info.Hash,
})
}
if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil {
return nil, fmt.Errorf("ensure folder path exists: %w", err)
}
@@ -315,7 +326,19 @@ func (r *DualReadWriter) MoveResource(ctx context.Context, opts DualWriteOptions
}
func (r *DualReadWriter) moveDirectory(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
// For directory moves, we just perform the repository move without parsing
// Reject directory move operations for configured branch - use bulk operations instead
if r.isConfiguredBranch(opts) {
return nil, &apierrors.StatusError{
ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusMethodNotAllowed,
Reason: metav1.StatusReasonMethodNotAllowed,
Message: "directory move operations are not available for configured branch. Use bulk move operations via the jobs API instead",
},
}
}
// For branch operations, we just perform the repository move without updating Grafana DB
// Always use the provisioning identity when writing
ctx, _, err := identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace)
if err != nil {
@@ -349,35 +372,6 @@ func (r *DualReadWriter) moveDirectory(ctx context.Context, opts DualWriteOption
},
}
// Handle folder management for main branch
if r.shouldUpdateGrafanaDB(opts, nil) {
// Ensure destination folder path exists
if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil {
return nil, fmt.Errorf("ensure destination folder path exists: %w", err)
}
// Try to delete the old folder structure from grafana (if it exists)
// This handles cleanup when folders are moved to new locations
oldFolderName, err := r.folders.EnsureFolderPathExist(ctx, opts.OriginalPath)
if err != nil {
return nil, fmt.Errorf("ensure original folder path exists: %w", err)
}
if oldFolderName != "" {
oldFolder, err := r.folders.GetFolder(ctx, oldFolderName)
if err != nil && !apierrors.IsNotFound(err) {
return nil, fmt.Errorf("get old folder for cleanup: %w", err)
}
if err == nil {
err = r.folders.Client().Delete(ctx, oldFolder.GetName(), metav1.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return nil, fmt.Errorf("delete old folder from storage: %w", err)
}
}
}
}
return parsed, nil
}
@@ -519,73 +513,62 @@ func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource,
}, parsed.Meta.GetFolder())
if err != nil || !rsp.Allowed {
return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(),
fmt.Errorf("no access to read the embedded file"))
fmt.Errorf("no access to perform %s on the resource", verb))
}
idType, _, err := authlib.ParseTypeID(id.GetID())
if err != nil {
return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), fmt.Errorf("could not determine identity type to check access"))
}
// only apply role based access if identity is not of type access policy
if idType == authlib.TypeAccessPolicy || id.GetOrgRole().Includes(identity.RoleEditor) {
return nil
}
return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(),
fmt.Errorf("must be admin or editor to access files from provisioning"))
return nil
}
func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, _ string) error {
func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, path string) error {
id, err := identity.GetRequester(ctx)
if err != nil {
return apierrors.NewUnauthorized(err.Error())
}
// Simple role based access for now
if id.GetOrgRole().Includes(identity.RoleEditor) {
return nil
// Determine parent folder from path
parentFolder := ""
if path != "" {
parentPath := safepath.Dir(path)
if parentPath != "" {
parentFolder = ParseFolder(parentPath, r.repo.Config().Name).ID
} else {
parentFolder = RootFolder(r.repo.Config())
}
}
return apierrors.NewForbidden(FolderResource.GroupResource(), "",
fmt.Errorf("must be admin or editor to access folders with provisioning"))
// For folder create operations, use empty name to check parent folder permissions
rsp, err := r.access.Check(ctx, id, authlib.CheckRequest{
Group: FolderResource.Group,
Resource: FolderResource.Resource,
Namespace: id.GetNamespace(),
Name: "", // Empty name for create operations
Verb: utils.VerbCreate,
}, parentFolder)
if err != nil || !rsp.Allowed {
return apierrors.NewForbidden(FolderResource.GroupResource(), path,
fmt.Errorf("no access to create folder in parent folder '%s'", parentFolder))
}
return nil
}
func (r *DualReadWriter) deleteFolder(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) {
// if the ref is set, it is not the active branch, so just delete the files from the branch
// and do not delete the items from grafana itself
if !r.shouldUpdateGrafanaDB(opts, nil) {
err := r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message)
if err != nil {
return nil, fmt.Errorf("error deleting folder from repository: %w", err)
// Reject directory delete operations for configured branch - use bulk operations instead
if r.isConfiguredBranch(opts) {
return nil, &apierrors.StatusError{
ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusMethodNotAllowed,
Reason: metav1.StatusReasonMethodNotAllowed,
Message: "directory delete operations are not available for configured branch. Use bulk delete operations via the jobs API instead",
},
}
return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo)
}
// before deleting from the repo, first get all children resources to delete from grafana afterwards
treeEntries, err := r.repo.ReadTree(ctx, "")
// For branch operations, just delete from the repository without updating Grafana DB
err := r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message)
if err != nil {
return nil, fmt.Errorf("read repository tree: %w", err)
}
// note: parsedFolders will include the folder itself
parsedResources, parsedFolders, err := r.getChildren(ctx, opts.Path, treeEntries)
if err != nil {
return nil, fmt.Errorf("parse resources in folder: %w", err)
}
// delete from the repo
err = r.repo.Delete(ctx, opts.Path, opts.Ref, opts.Message)
if err != nil {
return nil, fmt.Errorf("delete folder from repository: %w", err)
}
// delete from grafana
ctx, _, err = identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace)
if err != nil {
return nil, err
}
if err := r.deleteChildren(ctx, parsedResources, parsedFolders); err != nil {
return nil, fmt.Errorf("delete folder from grafana: %w", err)
return nil, fmt.Errorf("error deleting folder from repository: %w", err)
}
return folderDeleteResponse(ctx, opts.Path, opts.Ref, r.repo)
@@ -640,60 +623,11 @@ func folderDeleteResponse(ctx context.Context, path, ref string, repo repository
return parsed, nil
}
func (r *DualReadWriter) getChildren(ctx context.Context, folderPath string, treeEntries []repository.FileTreeEntry) ([]*ParsedResource, []Folder, error) {
var resourcesInFolder []repository.FileTreeEntry
var foldersInFolder []Folder
for _, entry := range treeEntries {
// make sure the path is supported (i.e. not ignored by git sync) and that the path is the folder itself or a child of the folder
if IsPathSupported(entry.Path) != nil || !safepath.InDir(entry.Path, folderPath) {
continue
}
// folders cannot be parsed as resources, so handle them separately
if entry.Blob {
resourcesInFolder = append(resourcesInFolder, entry)
} else {
folder := ParseFolder(entry.Path, r.repo.Config().Name)
foldersInFolder = append(foldersInFolder, folder)
}
}
parsedResources := make([]*ParsedResource, len(resourcesInFolder))
for i, entry := range resourcesInFolder {
fileInfo, err := r.repo.Read(ctx, entry.Path, "")
if err != nil && !apierrors.IsNotFound(err) {
return nil, nil, fmt.Errorf("could not find resource in repository: %w", err)
}
parsed, err := r.parser.Parse(ctx, fileInfo)
if err != nil {
return nil, nil, fmt.Errorf("could not parse resource: %w", err)
}
parsedResources[i] = parsed
}
return parsedResources, foldersInFolder, nil
}
func (r *DualReadWriter) deleteChildren(ctx context.Context, childrenResources []*ParsedResource, folders []Folder) error {
for _, parsed := range childrenResources {
err := parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to delete nested resource from grafana: %w", err)
}
}
// we need to delete the folders furthest down in the tree first, as folder deletion will fail if there is anything inside of it
safepath.SortByDepth(folders, func(f Folder) string { return f.Path }, false)
for _, f := range folders {
err := r.folders.Client().Delete(ctx, f.ID, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("failed to delete folder from grafana: %w", err)
}
}
return nil
// isConfiguredBranch returns true if the ref targets the configured branch
// (empty ref means configured branch, or ref explicitly matches configured branch)
func (r *DualReadWriter) isConfiguredBranch(opts DualWriteOptions) bool {
configuredBranch := r.repo.Config().Branch()
return opts.Ref == "" || opts.Ref == configuredBranch
}
// shouldUpdateGrafanaDB returns true if we have an empty ref (targeting the configured branch)
@@ -703,9 +637,5 @@ func (r *DualReadWriter) shouldUpdateGrafanaDB(opts DualWriteOptions, parsed *Pa
return false
}
if opts.Ref != "" && opts.Ref != opts.Branch {
return false
}
return true
return r.isConfiguredBranch(opts)
}
+1 -1
View File
@@ -37,7 +37,7 @@ var WireSetExts = wire.NewSet(
// Auditing Options
auditing.ProvideNoopBackend,
auditing.ProvideNoopPolicyRuleEvaluator,
auditing.ProvideNoopPolicyRuleProvider,
)
var provisioningExtras = wire.NewSet(
+150
View File
@@ -0,0 +1,150 @@
package advisor
import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
)
const (
// Check
ActionAdvisorCheckCreate = "advisor.checks:create" // CREATE.
ActionAdvisorCheckWrite = "advisor.checks:write" // UPDATE.
ActionAdvisorCheckRead = "advisor.checks:read" // GET + LIST.
ActionAdvisorCheckDelete = "advisor.checks:delete" // DELETE.
// CheckTypes
ActionAdvisorCheckTypesCreate = "advisor.checktypes:create" // CREATE.
ActionAdvisorCheckTypesWrite = "advisor.checktypes:write" // UPDATE.
ActionAdvisorCheckTypesRead = "advisor.checktypes:read" // GET + LIST.
ActionAdvisorCheckTypesDelete = "advisor.checktypes:delete" // DELETE.
// Register
ActionAdvisorRegisterCreate = "advisor.register:create" // CREATE (register check types).
)
var (
ScopeProviderAdvisorCheck = accesscontrol.NewScopeProvider("advisor.checks")
ScopeProviderAdvisorCheckTypes = accesscontrol.NewScopeProvider("advisor.checktypes")
ScopeProviderAdvisorRegister = accesscontrol.NewScopeProvider("advisor.register")
ScopeAllAdvisorCheck = ScopeProviderAdvisorCheck.GetResourceAllScope()
ScopeAllAdvisorCheckTypes = ScopeProviderAdvisorCheckTypes.GetResourceAllScope()
ScopeAllAdvisorRegister = ScopeProviderAdvisorRegister.GetResourceAllScope()
)
func registerAccessControlRoles(service accesscontrol.Service) error {
// Check
checkReader := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:advisor.checks:reader",
DisplayName: "Advisor Check Reader",
Description: "Read and list advisor checks.",
Group: "Advisor",
Permissions: []accesscontrol.Permission{
{
Action: ActionAdvisorCheckRead,
Scope: ScopeAllAdvisorCheck,
},
},
},
Grants: []string{string(org.RoleAdmin)},
}
checkWriter := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:advisor.checks:writer",
DisplayName: "Advisor Check Writer",
Description: "Create, update and delete advisor checks.",
Group: "Advisor",
Permissions: []accesscontrol.Permission{
{
Action: ActionAdvisorCheckCreate,
Scope: ScopeAllAdvisorCheck,
},
{
Action: ActionAdvisorCheckRead,
Scope: ScopeAllAdvisorCheck,
},
{
Action: ActionAdvisorCheckWrite,
Scope: ScopeAllAdvisorCheck,
},
{
Action: ActionAdvisorCheckDelete,
Scope: ScopeAllAdvisorCheck,
},
},
},
Grants: []string{string(org.RoleAdmin)},
}
// CheckTypes
checkTypesReader := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:advisor.checktypes:reader",
DisplayName: "Advisor Check Types Reader",
Description: "Read and list advisor check types.",
Group: "Advisor",
Permissions: []accesscontrol.Permission{
{
Action: ActionAdvisorCheckTypesRead,
Scope: ScopeAllAdvisorCheckTypes,
},
},
},
Grants: []string{string(org.RoleAdmin)},
}
checkTypesWriter := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:advisor.checktypes:writer",
DisplayName: "Advisor Check Types Writer",
Description: "Create, update and delete advisor check types.",
Group: "Advisor",
Permissions: []accesscontrol.Permission{
{
Action: ActionAdvisorCheckTypesCreate,
Scope: ScopeAllAdvisorCheckTypes,
},
{
Action: ActionAdvisorCheckTypesRead,
Scope: ScopeAllAdvisorCheckTypes,
},
{
Action: ActionAdvisorCheckTypesWrite,
Scope: ScopeAllAdvisorCheckTypes,
},
{
Action: ActionAdvisorCheckTypesDelete,
Scope: ScopeAllAdvisorCheckTypes,
},
},
},
Grants: []string{string(org.RoleAdmin)},
}
// Register
registerWriter := accesscontrol.RoleRegistration{
Role: accesscontrol.RoleDTO{
Name: "fixed:advisor.register:writer",
DisplayName: "Advisor Register Writer",
Description: "Register default advisor check types.",
Group: "Advisor",
Permissions: []accesscontrol.Permission{
{
Action: ActionAdvisorRegisterCreate,
Scope: ScopeAllAdvisorRegister,
},
},
},
Grants: []string{string(org.RoleAdmin)},
}
return service.DeclareFixedRoles(
checkReader,
checkWriter,
checkTypesReader,
checkTypesWriter,
registerWriter,
)
}
+16 -27
View File
@@ -1,17 +1,17 @@
package advisor
import (
"github.com/grafana/grafana-app-sdk/app"
"fmt"
authlib "github.com/grafana/authlib/types"
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
"github.com/grafana/grafana-app-sdk/simple"
advisorapi "github.com/grafana/grafana/apps/advisor/pkg/apis"
advisorapp "github.com/grafana/grafana/apps/advisor/pkg/app"
"github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/rest"
)
var (
@@ -20,37 +20,26 @@ var (
)
type AdvisorAppInstaller struct {
appsdkapiserver.AppInstaller
}
// GetAuthorizer returns the authorizer for the plugins app.
func (a *AdvisorAppInstaller) GetAuthorizer() authorizer.Authorizer {
return advisorapp.GetAuthorizer()
*advisorapp.AdvisorAppInstaller
}
func ProvideAppInstaller(
accessControlService accesscontrol.Service,
accessClient authlib.AccessClient,
checkRegistry checkregistry.CheckService,
cfg *setting.Cfg,
orgService org.Service,
) (*AdvisorAppInstaller, error) {
provider := simple.NewAppProvider(advisorapi.LocalManifest(), nil, advisorapp.New)
pluginConfig := cfg.PluginSettings["grafana-advisor-app"]
specificConfig := checkregistry.AdvisorAppConfig{
CheckRegistry: checkRegistry,
PluginConfig: pluginConfig,
StackID: cfg.StackID,
OrgService: orgService,
if err := registerAccessControlRoles(accessControlService); err != nil {
return nil, fmt.Errorf("registering access control roles: %w", err)
}
appCfg := app.Config{
KubeConfig: rest.Config{},
ManifestData: *advisorapi.LocalManifest().ManifestData,
SpecificConfig: specificConfig,
}
installer := &AdvisorAppInstaller{}
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appCfg, advisorapi.NewGoTypeAssociator())
authorizer := grafanaauthorizer.NewResourceAuthorizer(accessClient)
i, err := advisorapp.ProvideAppInstaller(authorizer, checkRegistry, cfg, orgService)
if err != nil {
return nil, err
}
installer.AppInstaller = i
return installer, nil
return &AdvisorAppInstaller{
AdvisorAppInstaller: i,
}, nil
}
@@ -110,10 +110,11 @@ func convertToK8sResource(
}
var permissionMapper = map[ngmodels.ReceiverPermission]string{
ngmodels.ReceiverPermissionReadSecret: "canReadSecrets",
ngmodels.ReceiverPermissionAdmin: "canAdmin",
ngmodels.ReceiverPermissionWrite: "canWrite",
ngmodels.ReceiverPermissionDelete: "canDelete",
ngmodels.ReceiverPermissionReadSecret: "canReadSecrets",
ngmodels.ReceiverPermissionAdmin: "canAdmin",
ngmodels.ReceiverPermissionWrite: "canWrite",
ngmodels.ReceiverPermissionDelete: "canDelete",
ngmodels.ReceiverPermissionModifyProtected: "canModifyProtected",
}
func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[string][]string, error) {
+2 -1
View File
@@ -2,9 +2,10 @@ module github.com/grafana/grafana/pkg/semconv
go 1.25.5
require go.opentelemetry.io/otel v1.38.0
require go.opentelemetry.io/otel v1.39.0
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
)
+4 -2
View File
@@ -1,3 +1,5 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -6,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1
View File
@@ -349,6 +349,7 @@ var wireBasicSet = wire.NewSet(
dashboardservice.ProvideDashboardService,
dashboardservice.ProvideDashboardProvisioningService,
dashboardservice.ProvideDashboardPluginService,
dashboardservice.ProvideDashboardAccessService,
dashboardstore.ProvideDashboardStore,
folderimpl.ProvideService,
wire.Bind(new(folder.Service), new(*folderimpl.Service)),
+11 -9
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -460,6 +460,7 @@ const (
ActionAlertingReceiversReadSecrets = "alert.notifications.receivers.secrets:read"
ActionAlertingReceiversCreate = "alert.notifications.receivers:create"
ActionAlertingReceiversUpdate = "alert.notifications.receivers:write"
ActionAlertingReceiversUpdateProtected = "alert.notifications.receivers.protected:write"
ActionAlertingReceiversDelete = "alert.notifications.receivers:delete"
ActionAlertingReceiversTest = "alert.notifications.receivers:test"
ActionAlertingReceiversPermissionsRead = "receivers.permissions:read"
@@ -23,7 +23,7 @@ import (
var ReceiversViewActions = []string{accesscontrol.ActionAlertingReceiversRead}
var ReceiversEditActions = append(ReceiversViewActions, []string{accesscontrol.ActionAlertingReceiversUpdate, accesscontrol.ActionAlertingReceiversDelete}...)
var ReceiversAdminActions = append(ReceiversEditActions, []string{accesscontrol.ActionAlertingReceiversReadSecrets, accesscontrol.ActionAlertingReceiversPermissionsRead, accesscontrol.ActionAlertingReceiversPermissionsWrite}...)
var ReceiversAdminActions = append(ReceiversEditActions, []string{accesscontrol.ActionAlertingReceiversReadSecrets, accesscontrol.ActionAlertingReceiversPermissionsRead, accesscontrol.ActionAlertingReceiversPermissionsWrite, accesscontrol.ActionAlertingReceiversUpdateProtected}...)
// defaultPermissions returns the default permissions for a newly created receiver.
func defaultPermissions() []accesscontrol.SetResourcePermissionCommand {
@@ -86,6 +86,9 @@ func newPermissionRegistry() *permissionRegistry {
"plugins": "plugins:id:",
"plugins.plugins": "plugins.plugins:uid:",
"plugins.metas": "plugins.metas:uid:",
"advisor.checks": "advisor.checks:uid:",
"advisor.checktypes": "advisor.checktypes:uid:",
"advisor.register": "advisor.register:uid:",
"provisioners": "provisioners:",
"reports": "reports:id:",
"permissions": "permissions:type:",
@@ -33,6 +33,7 @@ type api struct {
permissions []string
features featuremgmt.FeatureToggles
restConfigProvider apiserver.RestConfigProvider
logger log.Logger
}
func newApi(cfg *setting.Cfg, ac accesscontrol.AccessControl, router routing.RouteRegister, manager *Service, features featuremgmt.FeatureToggles, restConfigProvider apiserver.RestConfigProvider) *api {
@@ -41,7 +42,23 @@ func newApi(cfg *setting.Cfg, ac accesscontrol.AccessControl, router routing.Rou
for i := len(manager.permissions) - 1; i >= 0; i-- {
permissions = append(permissions, manager.permissions[i])
}
return &api{cfg, ac, router, manager, permissions, features, restConfigProvider}
return &api{
cfg: cfg,
ac: ac,
router: router,
service: manager,
permissions: permissions,
features: features,
restConfigProvider: restConfigProvider,
logger: log.New("resource-permissions-api"),
}
}
// shouldUseK8sAPIs returns true if both feature flags for K8s API redirect are enabled
func (a *api) shouldUseK8sAPIs() bool {
//nolint:staticcheck // not yet migrated to OpenFeature
return a.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthZHandlerRedirect) &&
a.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis)
}
func (a *api) registerEndpoints() {
@@ -189,11 +206,10 @@ func (a *api) getPermissions(c *contextmodel.ReqContext) response.Response {
return response.JSON(http.StatusOK, k8sPermissions)
}
span.RecordError(err)
logger := log.New("resource-permissions-api")
if errors.Is(err, ErrRestConfigNotAvailable) {
logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
} else {
logger.Warn("Failed to get resource permissions from k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
a.logger.Warn("Failed to get resource permissions from k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
}
}
@@ -304,6 +320,19 @@ func (a *api) setUserPermission(c *contextmodel.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if a.shouldUseK8sAPIs() {
err := a.setUserPermissionToK8s(c.Req.Context(), c.Namespace, resourceID, userID, cmd.Permission)
if err == nil {
return permissionSetResponse(cmd)
}
span.RecordError(err)
if errors.Is(err, ErrRestConfigNotAvailable) {
a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
} else {
a.logger.Warn("Failed to set user permission in k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
}
}
_, err = a.service.SetUserPermission(c.Req.Context(), c.GetOrgID(), accesscontrol.User{ID: userID}, resourceID, cmd.Permission)
if err != nil {
return response.Err(err)
@@ -361,6 +390,19 @@ func (a *api) setTeamPermission(c *contextmodel.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if a.shouldUseK8sAPIs() {
err := a.setTeamPermissionToK8s(c.Req.Context(), c.Namespace, resourceID, teamID, cmd.Permission)
if err == nil {
return permissionSetResponse(cmd)
}
span.RecordError(err)
if errors.Is(err, ErrRestConfigNotAvailable) {
a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
} else {
a.logger.Warn("Failed to set team permission in k8s API, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
}
}
_, err = a.service.SetTeamPermission(c.Req.Context(), c.GetOrgID(), teamID, resourceID, cmd.Permission)
if err != nil {
return response.Err(err)
@@ -415,6 +457,17 @@ func (a *api) setBuiltinRolePermission(c *contextmodel.ReqContext) response.Resp
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if a.shouldUseK8sAPIs() {
err := a.setBuiltInRolePermissionToK8s(c.Req.Context(), c.Namespace, resourceID, builtInRole, cmd.Permission)
if err == nil {
return permissionSetResponse(cmd)
}
span.RecordError(err)
if errors.Is(err, ErrRestConfigNotAvailable) {
a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
}
}
_, err := a.service.SetBuiltInRolePermission(c.Req.Context(), c.GetOrgID(), builtInRole, resourceID, cmd.Permission)
if err != nil {
return response.Err(err)
@@ -463,6 +516,17 @@ func (a *api) setPermissions(c *contextmodel.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "Bad request data: "+err.Error(), err)
}
if a.shouldUseK8sAPIs() {
err := a.setResourcePermissionsToK8s(c.Req.Context(), c.Namespace, resourceID, cmd.Permissions)
if err == nil {
return response.Success("Permissions updated")
}
span.RecordError(err)
if errors.Is(err, ErrRestConfigNotAvailable) {
a.logger.Debug("k8s API not available for resource permissions, falling back to legacy", "error", err, "resourceID", resourceID, "resource", a.service.options.Resource)
}
}
_, err := a.service.SetPermissions(ctx, c.GetOrgID(), resourceID, cmd.Permissions...)
if err != nil {
return response.Err(err)
@@ -13,9 +13,12 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
)
@@ -162,3 +165,231 @@ func getMapKeys(m map[string][]string) []string {
func (a *api) buildResourcePermissionName(resourceID string) string {
return fmt.Sprintf("%s-%s-%s", a.getAPIGroup(), a.service.options.Resource, resourceID)
}
// Write operations
func (a *api) setResourcePermissionsToK8s(ctx context.Context, namespace string, resourceID string, permissions []accesscontrol.SetResourcePermissionCommand) error {
dynamicClient, err := a.getDynamicClient(ctx)
if err != nil {
return err
}
resourcePermName := a.buildResourcePermissionName(resourceID)
resourcePermResource := dynamicClient.Resource(iamv0.ResourcePermissionInfo.GroupVersionResource()).Namespace(namespace)
_, existingResourceVersion, err := a.getExistingResourcePermission(ctx, resourcePermResource, resourcePermName)
if err != nil {
return err
}
k8sPermissions := make([]iamv0.ResourcePermissionspecPermission, 0, len(permissions))
for _, perm := range permissions {
if perm.Permission == "" {
continue
}
kind := a.getPermissionKind(perm)
name, err := a.getPermissionName(ctx, perm)
if err != nil {
return fmt.Errorf("failed to get permission name: %w", err)
}
k8sPermissions = append(k8sPermissions, iamv0.ResourcePermissionspecPermission{
Kind: iamv0.ResourcePermissionSpecPermissionKind(kind),
Name: name,
Verb: cases.Lower(language.Und).String(perm.Permission),
})
}
if len(k8sPermissions) == 0 {
if existingResourceVersion != "" {
err = resourcePermResource.Delete(ctx, resourcePermName, metav1.DeleteOptions{})
if err != nil && !k8serrors.IsNotFound(err) {
return fmt.Errorf("failed to delete resource permission in k8s: %w", err)
}
}
return nil
}
resourcePerm := &iamv0.ResourcePermission{
TypeMeta: metav1.TypeMeta{
APIVersion: iamv0.ResourcePermissionInfo.GroupVersion().String(),
Kind: iamv0.ResourcePermissionInfo.TypeMeta().Kind,
},
ObjectMeta: metav1.ObjectMeta{
Name: resourcePermName,
Namespace: namespace,
ResourceVersion: existingResourceVersion,
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: a.getAPIGroup(),
Resource: a.service.options.Resource,
Name: resourceID,
},
Permissions: k8sPermissions,
},
}
return a.createOrUpdateResourcePermission(ctx, resourcePermResource, resourcePerm, existingResourceVersion != "")
}
func (a *api) setUserPermissionToK8s(ctx context.Context, namespace string, resourceID string, userID int64, permission string) error {
userDetails, err := a.service.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userID})
if err != nil {
return fmt.Errorf("failed to get user details: %w", err)
}
return a.setSinglePermissionToK8s(ctx, namespace, resourceID, string(iamv0.ResourcePermissionSpecPermissionKindUser), userDetails.UID, permission)
}
func (a *api) setTeamPermissionToK8s(ctx context.Context, namespace string, resourceID string, teamID int64, permission string) error {
teamDetails, err := a.service.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{ID: teamID})
if err != nil {
return fmt.Errorf("failed to get team details: %w", err)
}
return a.setSinglePermissionToK8s(ctx, namespace, resourceID, string(iamv0.ResourcePermissionSpecPermissionKindTeam), teamDetails.UID, permission)
}
func (a *api) setBuiltInRolePermissionToK8s(ctx context.Context, namespace string, resourceID string, builtInRole string, permission string) error {
return a.setSinglePermissionToK8s(ctx, namespace, resourceID, string(iamv0.ResourcePermissionSpecPermissionKindBasicRole), builtInRole, permission)
}
func (a *api) setSinglePermissionToK8s(ctx context.Context, namespace string, resourceID string, kind string, name string, permission string) error {
dynamicClient, err := a.getDynamicClient(ctx)
if err != nil {
return err
}
resourcePermName := a.buildResourcePermissionName(resourceID)
resourcePermResource := dynamicClient.Resource(iamv0.ResourcePermissionInfo.GroupVersionResource()).Namespace(namespace)
existingResourcePerm, existingResourceVersion, err := a.getExistingResourcePermission(ctx, resourcePermResource, resourcePermName)
if err != nil {
return err
}
newPermissions := make([]iamv0.ResourcePermissionspecPermission, 0)
for _, perm := range existingResourcePerm.Spec.Permissions {
if string(perm.Kind) == kind && perm.Name == name {
continue
}
newPermissions = append(newPermissions, perm)
}
if permission != "" {
newPermissions = append(newPermissions, iamv0.ResourcePermissionspecPermission{
Kind: iamv0.ResourcePermissionSpecPermissionKind(kind),
Name: name,
Verb: cases.Lower(language.Und).String(permission),
})
}
if len(newPermissions) == 0 {
if existingResourceVersion != "" {
err = resourcePermResource.Delete(ctx, resourcePermName, metav1.DeleteOptions{})
if err != nil && !k8serrors.IsNotFound(err) {
return fmt.Errorf("failed to delete resource permission in k8s: %w", err)
}
}
return nil
}
resourcePerm := &iamv0.ResourcePermission{
TypeMeta: metav1.TypeMeta{
APIVersion: iamv0.ResourcePermissionInfo.GroupVersion().String(),
Kind: iamv0.ResourcePermissionInfo.TypeMeta().Kind,
},
ObjectMeta: metav1.ObjectMeta{
Name: resourcePermName,
Namespace: namespace,
ResourceVersion: existingResourceVersion,
},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: a.getAPIGroup(),
Resource: a.service.options.Resource,
Name: resourceID,
},
Permissions: newPermissions,
},
}
return a.createOrUpdateResourcePermission(ctx, resourcePermResource, resourcePerm, existingResourceVersion != "")
}
func (a *api) getPermissionKind(perm accesscontrol.SetResourcePermissionCommand) string {
if perm.UserID != 0 {
return string(iamv0.ResourcePermissionSpecPermissionKindUser)
}
if perm.TeamID != 0 {
return string(iamv0.ResourcePermissionSpecPermissionKindTeam)
}
if perm.BuiltinRole != "" {
return string(iamv0.ResourcePermissionSpecPermissionKindBasicRole)
}
return ""
}
func (a *api) getExistingResourcePermission(ctx context.Context, resourcePermResource dynamic.ResourceInterface, resourcePermName string) (*iamv0.ResourcePermission, string, error) {
unstructuredObj, err := resourcePermResource.Get(ctx, resourcePermName, metav1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
return &iamv0.ResourcePermission{}, "", nil
}
return nil, "", fmt.Errorf("failed to get existing resource permission: %w", err)
}
var resourcePerm iamv0.ResourcePermission
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.Object, &resourcePerm); err != nil {
return nil, "", fmt.Errorf("failed to convert existing resource permission: %w", err)
}
return &resourcePerm, unstructuredObj.GetResourceVersion(), nil
}
func (a *api) createOrUpdateResourcePermission(ctx context.Context, resourcePermResource dynamic.ResourceInterface, resourcePerm *iamv0.ResourcePermission, isUpdate bool) error {
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resourcePerm)
if err != nil {
return fmt.Errorf("failed to convert resource permission to unstructured: %w", err)
}
unstructuredPerm := &unstructured.Unstructured{Object: unstructuredObj}
if isUpdate {
_, err = resourcePermResource.Update(ctx, unstructuredPerm, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("failed to update resource permission in k8s: %w", err)
}
} else {
_, err = resourcePermResource.Create(ctx, unstructuredPerm, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create resource permission in k8s: %w", err)
}
}
return nil
}
func (a *api) getPermissionName(ctx context.Context, perm accesscontrol.SetResourcePermissionCommand) (string, error) {
if perm.UserID != 0 {
userDetails, err := a.service.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: perm.UserID})
if err != nil {
return "", fmt.Errorf("failed to get user details for user ID %d: %w", perm.UserID, err)
}
return userDetails.UID, nil
}
if perm.TeamID != 0 {
teamDetails, err := a.service.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{
ID: perm.TeamID,
})
if err != nil {
return "", fmt.Errorf("failed to get team details for team ID %d: %w", perm.TeamID, err)
}
return teamDetails.UID, nil
}
if perm.BuiltinRole != "" {
return perm.BuiltinRole, nil
}
return "", fmt.Errorf("no valid permission subject found")
}
@@ -0,0 +1,200 @@
package resourcepermissions
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
// TestGetPermissionKind tests the permission kind mapping logic
func TestGetPermissionKind(t *testing.T) {
api := &api{
service: &Service{
options: Options{
Resource: "dashboards",
ResourceAttribute: "uid",
},
},
}
tests := []struct {
name string
perm accesscontrol.SetResourcePermissionCommand
expected string
}{
{
name: "user permission",
perm: accesscontrol.SetResourcePermissionCommand{UserID: 123},
expected: string(iamv0.ResourcePermissionSpecPermissionKindUser),
},
{
name: "team permission",
perm: accesscontrol.SetResourcePermissionCommand{TeamID: 456},
expected: string(iamv0.ResourcePermissionSpecPermissionKindTeam),
},
{
name: "builtin role permission",
perm: accesscontrol.SetResourcePermissionCommand{BuiltinRole: "Editor"},
expected: string(iamv0.ResourcePermissionSpecPermissionKindBasicRole),
},
{
name: "empty permission returns empty kind",
perm: accesscontrol.SetResourcePermissionCommand{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kind := api.getPermissionKind(tt.perm)
assert.Equal(t, tt.expected, kind)
})
}
}
// TestGetDynamicClient_RestConfigNotAvailable tests error handling when rest config is not available
func TestGetDynamicClient_RestConfigNotAvailable(t *testing.T) {
ctx := context.Background()
api := &api{
service: &Service{
options: Options{
Resource: "dashboards",
},
},
restConfigProvider: nil,
}
client, err := api.getDynamicClient(ctx)
assert.Error(t, err)
assert.Nil(t, client)
assert.Equal(t, ErrRestConfigNotAvailable, err)
}
// TestBuildResourcePermissionName tests resource permission name building
func TestBuildResourcePermissionName(t *testing.T) {
tests := []struct {
name string
apiGroup string
resource string
resourceID string
expectedName string
}{
{
name: "with custom API group",
apiGroup: "dashboard.grafana.app",
resource: "dashboards",
resourceID: "dashboard-uid-123",
expectedName: "dashboard.grafana.app-dashboards-dashboard-uid-123",
},
{
name: "with default API group",
apiGroup: "",
resource: "folders",
resourceID: "folder-uid-456",
expectedName: "folders.grafana.app-folders-folder-uid-456",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
api := &api{
service: &Service{
options: Options{
Resource: tt.resource,
APIGroup: tt.apiGroup,
},
},
}
name := api.buildResourcePermissionName(tt.resourceID)
assert.Equal(t, tt.expectedName, name)
})
}
}
// TestGetAPIGroup tests API group resolution
func TestGetAPIGroup(t *testing.T) {
t.Run("returns custom API group when set", func(t *testing.T) {
api := &api{
service: &Service{
options: Options{
Resource: "dashboards",
APIGroup: "custom.grafana.app",
},
},
}
group := api.getAPIGroup()
assert.Equal(t, "custom.grafana.app", group)
})
t.Run("returns default API group when not set", func(t *testing.T) {
api := &api{
service: &Service{
options: Options{
Resource: "dashboards",
APIGroup: "",
},
},
}
group := api.getAPIGroup()
assert.Equal(t, "dashboards.grafana.app", group)
})
t.Run("default group for folders", func(t *testing.T) {
api := &api{
service: &Service{
options: Options{
Resource: "folders",
APIGroup: "",
},
},
}
group := api.getAPIGroup()
assert.Equal(t, "folders.grafana.app", group)
})
}
// TestResourcePermissionKindConstants verifies the kind constants match expected values
func TestResourcePermissionKindConstants(t *testing.T) {
tests := []struct {
name string
kind iamv0.ResourcePermissionSpecPermissionKind
expected string
}{
{
name: "User kind",
kind: iamv0.ResourcePermissionSpecPermissionKindUser,
expected: "User",
},
{
name: "Team kind",
kind: iamv0.ResourcePermissionSpecPermissionKindTeam,
expected: "Team",
},
{
name: "ServiceAccount kind",
kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount,
expected: "ServiceAccount",
},
{
name: "BasicRole kind",
kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole,
expected: "BasicRole",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, string(tt.kind))
})
}
}
+8
View File
@@ -9,6 +9,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
genericapiserver "k8s.io/apiserver/pkg/server"
@@ -59,6 +60,13 @@ type APIGroupAuthorizer interface {
GetAuthorizer() authorizer.Authorizer
}
// APIGroupAuditor allows different API groups to opt-in and provide their own auditing policy evaluator function.
// Auditing is only enabled if this is implemented. If no customization is needed, you can use the default evaluator,
// `pkg/apiserver/auditing.NewDefaultGrafanaPolicyRuleEvaluator()`.
type APIGroupAuditor interface {
GetPolicyRuleEvaluator() audit.PolicyRuleEvaluator
}
type APIGroupMutation interface {
// Mutate allows the builder to make changes to the object before it is persisted.
// Context is used only for timeout/deadline/cancellation and tracing information.
+27
View File
@@ -29,6 +29,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/kube-openapi/pkg/common"
"github.com/grafana/grafana/pkg/apiserver/auditing"
"github.com/grafana/grafana/pkg/apiserver/endpoints/filters"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
@@ -497,6 +498,32 @@ func AddPostStartHooks(
return nil
}
func EvaluatorPolicyRuleFromBuilders(builders []APIGroupBuilder) auditing.PolicyRuleEvaluators {
policyRuleEvaluators := make(auditing.PolicyRuleEvaluators, 0)
for _, b := range builders {
auditor, ok := b.(APIGroupAuditor)
if !ok {
continue
}
policyRuleEvaluator := auditor.GetPolicyRuleEvaluator()
if policyRuleEvaluator == nil {
continue
}
for _, gv := range GetGroupVersions(b) {
if gv.Empty() {
continue
}
policyRuleEvaluators[gv] = policyRuleEvaluator
}
}
return policyRuleEvaluators
}
func allowRegisteringResourceByInfo(allowedResources []string, name string) bool {
// trim any subresources from the name
name = strings.Split(name, "/")[0]
+6 -5
View File
@@ -28,6 +28,7 @@ import (
dataplaneaggregator "github.com/grafana/grafana/pkg/aggregator/apiserver"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apiserver/auditing"
grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db"
@@ -115,8 +116,8 @@ type service struct {
builderMetrics *builder.BuilderMetrics
dualWriterMetrics *grafanarest.DualWriterMetrics
auditBackend audit.Backend
auditPolicyRuleEvaluator audit.PolicyRuleEvaluator
auditBackend audit.Backend
auditPolicyRuleProvider auditing.PolicyRuleProvider
}
func ProvideService(
@@ -142,7 +143,7 @@ func ProvideService(
appInstallers []appsdkapiserver.AppInstaller,
builderMetrics *builder.BuilderMetrics,
auditBackend audit.Backend,
auditPolicyRuleEvaluator audit.PolicyRuleEvaluator,
auditPolicyRuleProvider auditing.PolicyRuleProvider,
) (*service, error) {
scheme := builder.ProvideScheme()
codecs := builder.ProvideCodecFactory(scheme)
@@ -174,7 +175,7 @@ func ProvideService(
builderMetrics: builderMetrics,
dualWriterMetrics: grafanarest.NewDualWriterMetrics(reg),
auditBackend: auditBackend,
auditPolicyRuleEvaluator: auditPolicyRuleEvaluator,
auditPolicyRuleProvider: auditPolicyRuleProvider,
}
// This will be used when running as a dskit service
s.NamedService = services.NewBasicService(s.start, s.running, nil).WithName(modules.GrafanaAPIServer)
@@ -365,7 +366,7 @@ func (s *service) start(ctx context.Context) error {
// Auditing Options
serverConfig.AuditBackend = s.auditBackend
serverConfig.AuditPolicyRuleEvaluator = s.auditPolicyRuleEvaluator
serverConfig.AuditPolicyRuleEvaluator = s.auditPolicyRuleProvider.PolicyRuleProvider(builder.EvaluatorPolicyRuleFromBuilders(s.builders))
// Add OpenAPI specs for each group+version (existing builders)
err = builder.SetupConfig(
+5
View File
@@ -301,6 +301,11 @@ func NewMapperRegistry() MapperRegistry {
"plugins": newResourceTranslation("plugins.plugins", "uid", false, nil),
"metas": newResourceTranslation("plugins.metas", "uid", false, nil),
},
"advisor.grafana.app": {
"checks": newResourceTranslation("advisor.checks", "uid", false, nil),
"checktypes": newResourceTranslation("advisor.checktypes", "uid", false, nil),
"register": newResourceTranslation("advisor.register", "uid", false, nil),
},
})
return mapper
@@ -58,6 +58,13 @@ const (
RelationGetPermissions string = "get_permissions"
RelationSetPermissions string = "set_permissions"
RelationCanGet string = "can_get"
RelationCanCreate string = "can_create"
RelationCanUpdate string = "can_update"
RelationCanDelete string = "can_delete"
RelationCanGetPermissions string = "can_get_permissions"
RelationCanSetPermissions string = "can_set_permissions"
RelationSubresourceSetView string = "resource_" + RelationSetView
RelationSubresourceSetEdit string = "resource_" + RelationSetEdit
RelationSubresourceSetAdmin string = "resource_" + RelationSetAdmin
@@ -134,6 +141,26 @@ var RelationToVerbMapping = map[string]string{
RelationSetPermissions: utils.VerbSetPermissions,
}
// FolderPermissionRelation returns the optimized folder relation for permission management.
func FolderPermissionRelation(relation string) string {
switch relation {
case RelationGet:
return RelationCanGet
case RelationCreate:
return RelationCanCreate
case RelationUpdate:
return RelationCanUpdate
case RelationDelete:
return RelationCanDelete
case RelationGetPermissions:
return RelationCanGetPermissions
case RelationSetPermissions:
return RelationCanSetPermissions
default:
return relation
}
}
func IsGroupResourceRelation(relation string) bool {
return isValidRelation(relation, RelationsGroupResource)
}
@@ -4,15 +4,21 @@ type folder
relations
define parent: [folder]
# Action sets
define view: [user, service-account, team#member, role#assignee] or edit or view from parent
define edit: [user, service-account, team#member, role#assignee] or admin or edit from parent
# Permission levels
define admin: [user, service-account, team#member, role#assignee] or admin from parent
define edit: [user, service-account, team#member, role#assignee] or edit from parent
define view: [user, service-account, team#member, role#assignee] or view from parent
define get: [user, service-account, team#member, role#assignee] or get from parent
define create: [user, service-account, team#member, role#assignee] or create from parent
define update: [user, service-account, team#member, role#assignee] or update from parent
define delete: [user, service-account, team#member, role#assignee] or delete from parent
define get_permissions: [user, service-account, team#member, role#assignee] or get_permissions from parent
define set_permissions: [user, service-account, team#member, role#assignee] or set_permissions from parent
define get: [user, service-account, team#member, role#assignee] or view or get from parent
define create: [user, service-account, team#member, role#assignee] or edit or create from parent
define update: [user, service-account, team#member, role#assignee] or edit or update from parent
define delete: [user, service-account, team#member, role#assignee] or edit or delete from parent
define get_permissions: [user, service-account, team#member, role#assignee] or admin or get_permissions from parent
define set_permissions: [user, service-account, team#member, role#assignee] or admin or set_permissions from parent
# Computed actions
define can_get: admin or edit or view or get
define can_create: admin or edit or create
define can_update: admin or edit or update
define can_delete: admin or edit or delete
define can_get_permissions: admin or get_permissions
define can_set_permissions: admin or set_permissions
@@ -0,0 +1,947 @@
package server
import (
"context"
"fmt"
"math/rand"
"testing"
"time"
authzv1 "github.com/grafana/authlib/authz/proto/v1"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
const (
benchNamespace = "default"
// Folder tree parameters
foldersPerLevel = 3
folderDepth = 7
// Other data generation parameters
numResources = 50000
numUsers = 1000
numTeams = 100
// Timeout for List operations
listTimeout = 30 * time.Second
// Resource type constants for benchmarks
benchDashboardGroup = "dashboard.grafana.app"
benchDashboardResource = "dashboards"
benchFolderGroup = "folder.grafana.app"
benchFolderResource = "folders"
// BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch.
batchCheckSize = 50
)
// benchmarkData holds all the generated test data for benchmarks
type benchmarkData struct {
folders []string // folder UIDs
folderDepths map[string]int // folder UID -> depth level
folderParents map[string]string // folder UID -> parent UID
folderDescendants map[string]int // folder UID -> number of descendants (including self)
foldersByDepth [][]string // folders grouped by depth level
resources []string // resource names
resourceFolders map[string]string // resource name -> folder UID
users []string // user identifiers (e.g., "user:1")
teams []string // team identifiers (e.g., "team:1")
// Pre-computed test scenarios
deepestFolder string // folder at max depth for worst-case tests
midDepthFolder string // folder at depth/2
shallowFolder string // folder at depth 1
rootFolder string // root level folder (depth 0)
largestRootFolder string // root folder with most descendants
largestRootDescCount int // number of descendants in largestRootFolder
maxDepth int // maximum depth in the tree
}
// generateFolderHierarchy creates a balanced tree of folders.
// Each folder has `childrenPerFolder` children, up to `depth` levels deep.
func generateFolderHierarchy(childrenPerFolder, depth int) ([]*openfgav1.TupleKey, *benchmarkData) {
// Calculate total folders: childrenPerFolder + childrenPerFolder^2 + ... + childrenPerFolder^(depth+1)
totalFolders := 0
levelSize := childrenPerFolder
for d := 0; d <= depth; d++ {
totalFolders += levelSize
levelSize *= childrenPerFolder
}
data := &benchmarkData{
folders: make([]string, 0, totalFolders),
folderDepths: make(map[string]int),
folderParents: make(map[string]string),
folderDescendants: make(map[string]int),
}
tuples := make([]*openfgav1.TupleKey, 0, totalFolders)
folderIdx := 0
// Track folders at each level for parent assignment
levelFolders := make([][]string, depth+1)
for i := range levelFolders {
levelFolders[i] = make([]string, 0)
}
// Create root level folders (depth 0)
for i := 0; i < childrenPerFolder; i++ {
folderUID := fmt.Sprintf("folder-%d", folderIdx)
data.folders = append(data.folders, folderUID)
data.folderDepths[folderUID] = 0
levelFolders[0] = append(levelFolders[0], folderUID)
folderIdx++
}
// Create folders at each subsequent depth level
for d := 1; d <= depth; d++ {
parentFolders := levelFolders[d-1]
// Each parent gets exactly childrenPerFolder children
for _, parentUID := range parentFolders {
for j := 0; j < childrenPerFolder; j++ {
folderUID := fmt.Sprintf("folder-%d", folderIdx)
data.folders = append(data.folders, folderUID)
data.folderDepths[folderUID] = d
data.folderParents[folderUID] = parentUID
levelFolders[d] = append(levelFolders[d], folderUID)
// Create parent relationship tuple
tuples = append(tuples, common.NewFolderParentTuple(folderUID, parentUID))
folderIdx++
}
}
}
// Set reference folders for different depth scenarios
data.rootFolder = levelFolders[0][0]
data.shallowFolder = levelFolders[0][0]
if len(levelFolders[1]) > 0 {
data.shallowFolder = levelFolders[1][0]
}
midDepth := depth / 2
if len(levelFolders[midDepth]) > 0 {
data.midDepthFolder = levelFolders[midDepth][0]
}
// Deepest folder
if len(levelFolders[depth]) > 0 {
data.deepestFolder = levelFolders[depth][0]
}
// Calculate descendant counts for each folder (bottom-up)
// Initialize all folders with count of 1 (self)
for _, folder := range data.folders {
data.folderDescendants[folder] = 1
}
// Process folders from deepest to shallowest, accumulating descendant counts
for d := depth; d >= 0; d-- {
for _, folder := range levelFolders[d] {
if parent, hasParent := data.folderParents[folder]; hasParent {
data.folderDescendants[parent] += data.folderDescendants[folder]
}
}
}
// Find root folder with most descendants
for _, rootFolder := range levelFolders[0] {
count := data.folderDescendants[rootFolder]
if count > data.largestRootDescCount {
data.largestRootDescCount = count
data.largestRootFolder = rootFolder
}
}
// Store folders by depth for depth-based testing
data.foldersByDepth = levelFolders
data.maxDepth = depth
return tuples, data
}
// generateResources creates resources distributed across folders
func generateResources(data *benchmarkData, numResources int) []*openfgav1.TupleKey {
data.resources = make([]string, numResources)
data.resourceFolders = make(map[string]string, numResources)
// Distribute resources across folders
for i := 0; i < numResources; i++ {
resourceName := fmt.Sprintf("resource-%d", i)
folderIdx := i % len(data.folders)
folderUID := data.folders[folderIdx]
data.resources[i] = resourceName
data.resourceFolders[resourceName] = folderUID
}
// Note: We don't create tuples for resources themselves,
// permissions are assigned to users/teams on folders or directly on resources
return nil
}
// generateUsers creates user identifiers
func generateUsers(data *benchmarkData, numUsers int) {
data.users = make([]string, numUsers)
for i := 0; i < numUsers; i++ {
data.users[i] = fmt.Sprintf("user:%d", i)
}
}
// generateTeams creates team identifiers
func generateTeams(data *benchmarkData, numTeams int) {
data.teams = make([]string, numTeams)
for i := 0; i < numTeams; i++ {
data.teams[i] = fmt.Sprintf("team:%d", i)
}
}
// generatePermissionTuples creates various permission assignments for benchmarking.
// Users are distributed across 7 patterns: global, root folder, mid-depth folder,
// folder-scoped resource, direct resource, team-based, and no permissions.
const numPermissionPatterns = 7
func generatePermissionTuples(data *benchmarkData) []*openfgav1.TupleKey {
tuples := make([]*openfgav1.TupleKey, 0)
// Distribute users across different permission patterns
usersPerPattern := len(data.users) / numPermissionPatterns
// Pattern 1: Users with GroupResource permission (all access)
// Users 0 to usersPerPattern-1
for i := 0; i < usersPerPattern; i++ {
tuples = append(tuples, common.NewGroupResourceTuple(
data.users[i],
common.RelationGet,
benchDashboardGroup,
benchDashboardResource,
"",
))
}
// Pattern 2: Users with folder-level permission on root folders
// Users usersPerPattern to 2*usersPerPattern-1
for i := usersPerPattern; i < 2*usersPerPattern; i++ {
folderIdx := (i - usersPerPattern) % len(data.folders)
// Only assign to root-level folders for this pattern
for j := folderIdx; j < len(data.folders); j++ {
if data.folderDepths[data.folders[j]] == 0 {
tuples = append(tuples, common.NewFolderTuple(
data.users[i],
common.RelationSetView,
data.folders[j],
))
break
}
}
}
// Pattern 3: Users with folder-level permission on mid-depth folders
// Use relative depth range: 1/3 to 2/3 of max depth
// Use "view" relation which grants get through the optimized schema
minMidDepth := data.maxDepth / 3
maxMidDepth := 2 * data.maxDepth / 3
if maxMidDepth < minMidDepth {
maxMidDepth = minMidDepth
}
// Collect folders in the mid-depth range
var midDepthFolders []string
for d := minMidDepth; d <= maxMidDepth; d++ {
if d < len(data.foldersByDepth) {
midDepthFolders = append(midDepthFolders, data.foldersByDepth[d]...)
}
}
// Fall back to root folders if no mid-depth folders exist
if len(midDepthFolders) == 0 {
midDepthFolders = data.foldersByDepth[0]
}
for i := 2 * usersPerPattern; i < 3*usersPerPattern; i++ {
folderIdx := (i - 2*usersPerPattern) % len(midDepthFolders)
tuples = append(tuples, common.NewFolderTuple(
data.users[i],
common.RelationSetView,
midDepthFolders[folderIdx],
))
}
// Pattern 4: Users with folder-scoped resource permission
for i := 3 * usersPerPattern; i < 4*usersPerPattern; i++ {
folderIdx := (i - 3*usersPerPattern) % len(data.folders)
tuples = append(tuples, common.NewFolderResourceTuple(
data.users[i],
common.RelationGet,
benchDashboardGroup,
benchDashboardResource,
"",
data.folders[folderIdx],
))
}
// Pattern 5: Users with direct resource permission
for i := 4 * usersPerPattern; i < 5*usersPerPattern; i++ {
resourceIdx := (i - 4*usersPerPattern) % len(data.resources)
tuples = append(tuples, common.NewResourceTuple(
data.users[i],
common.RelationGet,
benchDashboardGroup,
benchDashboardResource,
"",
data.resources[resourceIdx],
))
}
// Pattern 6: Team memberships and team permissions
// First, add users to teams
for i := 5 * usersPerPattern; i < 6*usersPerPattern && i < len(data.users); i++ {
teamIdx := (i - 5*usersPerPattern) % len(data.teams)
tuples = append(tuples, common.NewTypedTuple(
common.TypeTeam,
data.users[i],
common.RelationTeamMember,
fmt.Sprintf("%d", teamIdx),
))
}
// Then, give teams folder permissions
// Use "view" relation which grants get through the optimized schema
for i := 0; i < len(data.teams); i++ {
folderIdx := i % len(data.folders)
teamMember := fmt.Sprintf("team:%d#member", i)
tuples = append(tuples, common.NewFolderTuple(
teamMember,
common.RelationSetView,
data.folders[folderIdx],
))
}
// Pattern 7: Users with no permissions (remaining users)
// These users don't get any tuples - they're for testing denial cases
return tuples
}
// setupBenchmarkServer creates a server with the benchmark data loaded
func setupBenchmarkServer(b *testing.B) (*Server, *benchmarkData) {
b.Helper()
if testing.Short() {
b.Skip("skipping benchmark in short mode")
}
cfg := setting.NewCfg()
testStore := sqlstore.NewTestStore(b, sqlstore.WithCfg(cfg))
openFGAStore, err := store.NewEmbeddedStore(cfg, testStore, log.NewNopLogger())
require.NoError(b, err)
openfga, err := NewOpenFGAServer(cfg.ZanzanaServer, openFGAStore)
require.NoError(b, err)
srv, err := NewServer(cfg.ZanzanaServer, openfga, log.NewNopLogger(), tracing.NewNoopTracerService(), prometheus.NewRegistry())
require.NoError(b, err)
// Generate test data
b.Log("Generating folder hierarchy...")
folderTuples, data := generateFolderHierarchy(foldersPerLevel, folderDepth)
b.Log("Generating resources...")
generateResources(data, numResources)
b.Log("Generating users...")
generateUsers(data, numUsers)
b.Log("Generating teams...")
generateTeams(data, numTeams)
b.Log("Generating permission tuples...")
permTuples := generatePermissionTuples(data)
// Add special user with permission on largest root folder (for >1000 folder test)
// Use "view" relation which grants get through the optimized schema
largeRootUserTuple := common.NewFolderTuple(
"user:large-root-access",
common.RelationSetView,
data.largestRootFolder,
)
permTuples = append(permTuples, largeRootUserTuple)
// Add users with permissions at each depth level for depth-based testing
// Use "view" relation which grants get through the optimized schema
for depth := 0; depth <= data.maxDepth; depth++ {
if len(data.foldersByDepth[depth]) == 0 {
continue
}
folder := data.foldersByDepth[depth][0]
user := fmt.Sprintf("user:depth-%d-access", depth)
permTuples = append(permTuples, common.NewFolderTuple(user, common.RelationSetView, folder))
}
// Combine all tuples
allTuples := append(folderTuples, permTuples...)
b.Logf("Total tuples to write: %d", len(allTuples))
// Get store info
ctx := newContextWithNamespace()
storeInf, err := srv.getStoreInfo(ctx, benchNamespace)
require.NoError(b, err)
// Write tuples in batches (OpenFGA limits to 100 per write)
batchSize := 100
for i := 0; i < len(allTuples); i += batchSize {
end := i + batchSize
if end > len(allTuples) {
end = len(allTuples)
}
batch := allTuples[i:end]
_, err = srv.openfga.Write(ctx, &openfgav1.WriteRequest{
StoreId: storeInf.ID,
AuthorizationModelId: storeInf.ModelID,
Writes: &openfgav1.WriteRequestWrites{
TupleKeys: batch,
OnDuplicate: "ignore",
},
})
require.NoError(b, err)
if (i/batchSize)%100 == 0 {
b.Logf("Written %d/%d tuples", end, len(allTuples))
}
}
b.Logf("Benchmark data setup complete: %d folders, %d resources, %d users, %d teams",
len(data.folders), len(data.resources), len(data.users), len(data.teams))
b.Logf("Largest root folder: %s with %d descendants", data.largestRootFolder, data.largestRootDescCount)
return srv, data
}
// BenchmarkCheck measures the performance of Check requests
func BenchmarkCheck(b *testing.B) {
srv, data := setupBenchmarkServer(b)
ctx := newContextWithNamespace()
// Helper to create check requests
newCheckReq := func(subject, verb, group, resource, folder, name string) *authzv1.CheckRequest {
return &authzv1.CheckRequest{
Namespace: benchNamespace,
Subject: subject,
Verb: verb,
Group: group,
Resource: resource,
Folder: folder,
Name: name,
}
}
usersPerPattern := len(data.users) / 7
b.Run("GroupResourceDirect", func(b *testing.B) {
// User with group_resource permission - should have access to everything
user := data.users[0] // First user has GroupResource permission
resource := data.resources[rand.Intn(len(data.resources))]
folder := data.resourceFolders[resource]
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource))
if err != nil {
b.Fatal(err)
}
if !res.GetAllowed() {
b.Fatal("expected access to be allowed")
}
}
})
// Test folder inheritance at each depth level (0 to maxDepth)
// User has permission on ROOT folder (depth 0), we check access at each deeper level
rootUser := "user:depth-0-access" // has view permission on root folder
for depth := 0; depth <= data.maxDepth; depth++ {
depth := depth // capture for closure
if len(data.foldersByDepth[depth]) == 0 {
continue
}
b.Run(fmt.Sprintf("FolderInheritance/Depth%d", depth), func(b *testing.B) {
resource := data.resources[0]
folder := data.foldersByDepth[depth][0]
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.Check(ctx, newCheckReq(rootUser, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource))
if err != nil {
b.Fatal(err)
}
_ = res.GetAllowed()
}
})
}
b.Run("FolderResourceScoped", func(b *testing.B) {
// User with folder-scoped resource permission
user := data.users[3*usersPerPattern]
folderIdx := 0
folder := data.folders[folderIdx]
resource := data.resources[folderIdx]
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource))
if err != nil {
b.Fatal(err)
}
_ = res.GetAllowed()
}
})
b.Run("DirectResource", func(b *testing.B) {
// User with direct resource permission
user := data.users[4*usersPerPattern]
resourceIdx := 0
resource := data.resources[resourceIdx]
folder := data.resourceFolders[resource]
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource))
if err != nil {
b.Fatal(err)
}
_ = res.GetAllowed()
}
})
b.Run("TeamMembership", func(b *testing.B) {
// User who is a team member, team has folder permission
user := data.users[5*usersPerPattern]
teamIdx := 0
folderIdx := teamIdx % len(data.folders)
folder := data.folders[folderIdx]
resource := data.resources[folderIdx%len(data.resources)]
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource))
if err != nil {
b.Fatal(err)
}
_ = res.GetAllowed()
}
})
b.Run("NoAccess", func(b *testing.B) {
// User with no permissions - tests denial path
user := data.users[len(data.users)-1] // Last user has no permissions
resource := data.resources[0]
folder := data.resourceFolders[resource]
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource, folder, resource))
if err != nil {
b.Fatal(err)
}
if res.GetAllowed() {
b.Fatal("expected access to be denied")
}
}
})
b.Run("FolderCheck", func(b *testing.B) {
// Direct folder access check
user := data.users[usersPerPattern]
folder := data.rootFolder
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.Check(ctx, newCheckReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource, "", folder))
if err != nil {
b.Fatal(err)
}
_ = res.GetAllowed()
}
})
}
func BenchmarkBatchCheck(b *testing.B) {
srv, data := setupBenchmarkServer(b)
ctx := newContextWithNamespace()
// Helper to create batch check requests
newBatchCheckReq := func(subject string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
return &authzextv1.BatchCheckRequest{
Namespace: benchNamespace,
Subject: subject,
Items: items,
}
}
// Helper to create batch items for resources in folders
createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzextv1.BatchCheckItem {
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
for i := 0; i < batchCheckSize && i < len(resources); i++ {
resource := resources[i]
items = append(items, &authzextv1.BatchCheckItem{
Verb: utils.VerbGet,
Group: benchDashboardGroup,
Resource: benchDashboardResource,
Name: resource,
Folder: resourceFolders[resource],
})
}
return items
}
// Helper to create batch items for folders at a specific depth
createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzextv1.BatchCheckItem {
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
for _, folder := range folders {
if folderDepths[folder] == depth && len(items) < batchCheckSize {
items = append(items, &authzextv1.BatchCheckItem{
Verb: utils.VerbGet,
Group: benchDashboardGroup,
Resource: benchDashboardResource,
Name: fmt.Sprintf("resource-in-%s", folder),
Folder: folder,
})
}
}
// Fill remaining slots if needed
for len(items) < batchCheckSize && len(folders) > 0 {
folder := folders[len(items)%len(folders)]
items = append(items, &authzextv1.BatchCheckItem{
Verb: utils.VerbGet,
Group: benchDashboardGroup,
Resource: benchDashboardResource,
Name: fmt.Sprintf("resource-%d", len(items)),
Folder: folder,
})
}
return items
}
usersPerPattern := len(data.users) / numPermissionPatterns
b.Run("GroupResourceDirect", func(b *testing.B) {
// User with group_resource permission - should have access to everything
user := data.users[0]
items := createBatchItems(data.resources, data.resourceFolders)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
b.Run("FolderInheritance/Depth1", func(b *testing.B) {
// User with folder permission on shallow folder
user := data.users[usersPerPattern]
items := createFolderBatchItems(data.folders, 1, data.folderDepths)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
b.Run("FolderInheritance/Depth4", func(b *testing.B) {
// User with folder permission on mid-depth folder
user := data.users[2*usersPerPattern]
items := createFolderBatchItems(data.folders, 4, data.folderDepths)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
b.Run("FolderInheritance/Depth7", func(b *testing.B) {
// Check access on deepest folders (worst case for inheritance traversal)
user := data.users[usersPerPattern]
items := createFolderBatchItems(data.folders, data.maxDepth, data.folderDepths)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
b.Run("DirectResource", func(b *testing.B) {
// User with direct resource permission
user := data.users[4*usersPerPattern]
items := createBatchItems(data.resources, data.resourceFolders)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
b.Run("TeamMembership", func(b *testing.B) {
// User who is a team member, team has folder permission
user := data.users[5*usersPerPattern]
items := createBatchItems(data.resources, data.resourceFolders)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
b.Run("NoAccess", func(b *testing.B) {
// User with no permissions - tests denial path
user := data.users[len(data.users)-1]
items := createBatchItems(data.resources, data.resourceFolders)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
b.Run("MixedFolders", func(b *testing.B) {
// Batch of items across different folder depths
user := data.users[usersPerPattern]
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
for i := 0; i < batchCheckSize; i++ {
folder := data.folders[i%len(data.folders)]
items = append(items, &authzextv1.BatchCheckItem{
Verb: utils.VerbGet,
Group: benchDashboardGroup,
Resource: benchDashboardResource,
Name: fmt.Sprintf("resource-%d", i),
Folder: folder,
})
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
if err != nil {
b.Fatal(err)
}
_ = res.Groups
}
})
}
// BenchmarkList measures the performance of List requests (Compile equivalent)
func BenchmarkList(b *testing.B) {
srv, data := setupBenchmarkServer(b)
baseCtx := newContextWithNamespace()
// Helper to create list requests
newListReq := func(subject, verb, group, resource string) *authzv1.ListRequest {
return &authzv1.ListRequest{
Namespace: benchNamespace,
Subject: subject,
Verb: verb,
Group: group,
Resource: resource,
}
}
// Helper to create context with timeout
ctxWithTimeout := func() (context.Context, context.CancelFunc) {
return context.WithTimeout(baseCtx, listTimeout)
}
usersPerPattern := len(data.users) / 7
b.Run("AllAccess", func(b *testing.B) {
// User with group_resource permission - should return All=true quickly
user := data.users[0]
b.Logf("Test: User with group_resource permission (access to ALL dashboards)")
b.Logf("Expected: All=true returned immediately without ListObjects call")
b.ResetTimer()
for i := 0; i < b.N; i++ {
ctx, cancel := ctxWithTimeout()
res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource))
cancel()
if err != nil {
b.Fatalf("Error: %v", err)
}
if !res.GetAll() {
b.Fatal("expected All=true for user with group_resource permission")
}
}
})
b.Run("FolderScoped", func(b *testing.B) {
// User with folder permissions - should return folder list
user := data.users[usersPerPattern]
b.Logf("Test: User with direct folder permission on a single folder")
b.Logf("Expected: Returns list of folders user has access to")
b.ResetTimer()
for i := 0; i < b.N; i++ {
ctx, cancel := ctxWithTimeout()
res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource))
cancel()
if err != nil {
b.Fatalf("Error: %v", err)
}
if i == 0 {
b.Logf("Result: %d folders, %d items, All=%v", len(res.GetFolders()), len(res.GetItems()), res.GetAll())
}
}
})
b.Run("DirectResources", func(b *testing.B) {
// User with direct resource permissions - should return items list
user := data.users[4*usersPerPattern]
b.Logf("Test: User with direct permission on specific resources")
b.Logf("Expected: Returns list of specific resources user has access to")
b.ResetTimer()
for i := 0; i < b.N; i++ {
ctx, cancel := ctxWithTimeout()
res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource))
cancel()
if err != nil {
b.Fatalf("Error: %v", err)
}
if i == 0 {
b.Logf("Result: %d folders, %d items, All=%v", len(res.GetFolders()), len(res.GetItems()), res.GetAll())
}
}
})
b.Run("NoAccess", func(b *testing.B) {
// User with no permissions - should return empty results
user := data.users[len(data.users)-1]
b.Logf("Test: User with NO permissions (denial case)")
b.Logf("Expected: Empty results")
b.ResetTimer()
for i := 0; i < b.N; i++ {
ctx, cancel := ctxWithTimeout()
res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchDashboardGroup, benchDashboardResource))
cancel()
if err != nil {
b.Fatalf("Error: %v", err)
}
if i == 0 {
b.Logf("Result: %d folders, %d items, All=%v", len(res.GetFolders()), len(res.GetItems()), res.GetAll())
}
}
})
b.Run("LargeRootFolder", func(b *testing.B) {
// User with access to root folder that has many descendants
user := "user:large-root-access"
b.Logf("Test: User with permission on ROOT folder (folder-0)")
b.Logf("Root folder %s has %d total descendants", data.largestRootFolder, data.largestRootDescCount)
b.Logf("Expected: ListObjects should return folders through inheritance")
b.ResetTimer()
for i := 0; i < b.N; i++ {
ctx, cancel := ctxWithTimeout()
start := time.Now()
res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource))
elapsed := time.Since(start)
cancel()
if err != nil {
b.Fatalf("Error after %v: %v", elapsed, err)
}
if i == 0 {
b.Logf("Result: %d folders returned in %v (descendants: %d)",
len(res.GetItems()), elapsed, data.largestRootDescCount)
}
}
})
// Test List at various folder depths to find breaking point
b.Run("ByDepth", func(b *testing.B) {
b.Logf("Testing List performance at various folder depths (timeout: %v)", listTimeout)
b.Logf("Tree structure: %d folders per level, %d max depth", foldersPerLevel, data.maxDepth)
for depth := 0; depth <= data.maxDepth; depth++ {
if len(data.foldersByDepth[depth]) == 0 {
continue
}
folder := data.foldersByDepth[depth][0]
descendants := data.folderDescendants[folder]
user := fmt.Sprintf("user:depth-%d-access", depth)
b.Run(fmt.Sprintf("Depth%d_%dDescendants", depth, descendants), func(b *testing.B) {
b.Logf("Test: User with permission on folder at depth %d", depth)
b.Logf("Folder: %s, Descendants: %d", folder, descendants)
// First, do a single timed run to report
ctx, cancel := ctxWithTimeout()
start := time.Now()
res, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource))
elapsed := time.Since(start)
cancel()
if err != nil {
b.Logf("FAILED after %v: %v", elapsed, err)
if elapsed >= listTimeout {
b.Logf("TIMEOUT: List took longer than %v", listTimeout)
}
b.Skip("Skipping benchmark iterations due to error")
return
}
b.Logf("Result: %d folders in %v", len(res.GetItems()), elapsed)
if elapsed > 5*time.Second {
b.Logf("WARNING: Single List took %v, skipping benchmark iterations", elapsed)
b.Skip("Too slow for benchmark iterations")
return
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ctx, cancel := ctxWithTimeout()
_, err := srv.List(ctx, newListReq(user, utils.VerbGet, benchFolderGroup, benchFolderResource))
cancel()
if err != nil {
b.Fatalf("Error: %v", err)
}
}
})
}
})
}
@@ -126,8 +126,14 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation string, resou
return &authzv1.CheckResponse{Allowed: false}, nil
}
// Use optimized folder permission relations for permission management
checkRelation := relation
if resource.Type() == common.TypeFolder {
checkRelation = common.FolderPermissionRelation(relation)
}
// Check if subject has direct access to resource
res, err := s.openfgaCheck(ctx, store, subject, relation, resourceIdent, contextuals, nil)
res, err := s.openfgaCheck(ctx, store, subject, checkRelation, resourceIdent, contextuals, nil)
if err != nil {
return nil, err
}
@@ -143,14 +149,15 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation string, res
defer span.End()
var (
folderIdent = resource.FolderIdent()
resourceCtx = resource.Context()
folderRelation = common.SubresourceRelation(relation)
folderIdent = resource.FolderIdent()
resourceCtx = resource.Context()
folderRelation = common.SubresourceRelation(relation)
folderCheckRelation = common.FolderPermissionRelation(relation)
)
if folderIdent != "" && isFolderPermissionBasedResource(resource.GroupResource()) {
// Check if resource inherits permissions from the folder (like dashboards in a folder)
res, err := s.openfgaCheck(ctx, store, subject, relation, folderIdent, contextuals, resourceCtx)
res, err := s.openfgaCheck(ctx, store, subject, folderCheckRelation, folderIdent, contextuals, resourceCtx)
if err != nil {
return nil, err
}
@@ -85,6 +85,12 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour
resourceCtx = resource.Context()
)
// Use optimized folder permission relations for permission management
listRelation := relation
if resource.Type() == common.TypeFolder {
listRelation = common.FolderPermissionRelation(relation)
}
var items []string
if resource.HasSubresource() && common.IsSubresourceRelation(subresourceRelation) {
// List requested subresources
@@ -110,7 +116,7 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, resour
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
Type: resource.Type(),
Relation: relation,
Relation: listRelation,
User: subject,
ContextualTuples: contextuals,
})
@@ -129,8 +135,9 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation string, reso
defer span.End()
var (
folderRelation = common.SubresourceRelation(relation)
resourceCtx = resource.Context()
folderRelation = common.SubresourceRelation(relation)
folderListRelation = common.FolderPermissionRelation(relation) // Optimized for permission management
resourceCtx = resource.Context()
)
// 1. List all folders subject has access to resource type in
@@ -159,7 +166,7 @@ func (s *Server) listGeneric(ctx context.Context, subject, relation string, reso
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
Type: common.TypeFolder,
Relation: relation,
Relation: folderListRelation,
User: subject,
Context: resourceCtx,
ContextualTuples: contextuals,
+5
View File
@@ -44,6 +44,11 @@ type DashboardService interface {
GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error)
}
type DashboardAccessService interface {
// The user as access to {VERB} the requested dashboard
HasDashboardAccess(ctx context.Context, user identity.Requester, verb string, namespace string, name string) (bool, error)
}
type PermissionsRegistrationService interface {
RegisterDashboardPermissions(service accesscontrol.DashboardPermissionsService)
@@ -5,9 +5,10 @@ package dashboards
import (
context "context"
identity "github.com/grafana/grafana/pkg/apimachinery/identity"
mock "github.com/stretchr/testify/mock"
identity "github.com/grafana/grafana/pkg/apimachinery/identity"
model "github.com/grafana/grafana/pkg/services/search/model"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -529,6 +530,11 @@ func (_m *FakeDashboardService) ValidateDashboardRefreshInterval(minRefreshInter
return r0
}
// CanViewDashboard uses the access control service to check if the requested user can see a dashboard
func (_m *FakeDashboardService) HasDashboardAccess(ctx context.Context, user identity.Requester, verb string, namespace string, name string) (bool, error) {
return true, nil
}
// NewFakeDashboardService creates a new instance of FakeDashboardService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewFakeDashboardService(t interface {
@@ -67,6 +67,7 @@ var (
_ dashboards.DashboardService = (*DashboardServiceImpl)(nil)
_ dashboards.DashboardProvisioningService = (*DashboardServiceImpl)(nil)
_ dashboards.PluginService = (*DashboardServiceImpl)(nil)
_ dashboards.DashboardAccessService = (*DashboardServiceImpl)(nil)
daysInTrash = 24 * 30 * time.Hour
tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards/service")
@@ -100,6 +101,38 @@ type DashboardServiceImpl struct {
dashboardPermissionsReady chan struct{}
}
// CanViewDashboard uses the access control service to check if the requested user can see a dashboard
func (dr *DashboardServiceImpl) HasDashboardAccess(ctx context.Context, user identity.Requester, verb string, namespace string, name string) (bool, error) {
ns, err := claims.ParseNamespace(namespace)
if err != nil {
return false, err
}
dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{
UID: name,
OrgID: ns.OrgID,
})
if err != nil || dash == nil {
return false, nil
}
var action string
switch verb {
case utils.VerbGet:
action = dashboards.ActionDashboardsRead
case utils.VerbUpdate:
action = dashboards.ActionDashboardsWrite
default:
return false, fmt.Errorf("unsupported verb")
}
evaluator := accesscontrol.EvalPermission(action,
dashboards.ScopeDashboardsProvider.GetResourceScopeUID(name))
canView, err := dr.ac.Evaluate(ctx, user, evaluator)
if err != nil || !canView {
return false, nil
}
return true, nil
}
func (dr *DashboardServiceImpl) startK8sDeletedDashboardsCleanupJob(ctx context.Context) chan struct{} {
done := make(chan struct{})
go func() {
@@ -23,3 +23,9 @@ func ProvideDashboardPluginService(
) dashboards.PluginService {
return orig
}
func ProvideDashboardAccessService(
features featuremgmt.FeatureToggles, orig *DashboardServiceImpl,
) dashboards.DashboardAccessService {
return orig
}
@@ -274,6 +274,11 @@ func (s *Service) listDashboardVersionsThroughK8s(
continueToken = tempOut.GetContinue()
}
// Update the continue token on the response to reflect the actual position after all fetched items.
// Without this, the response would return the token from the first fetch, causing duplicate items
// on subsequent pages when multiple fetches were needed to fill the requested limit.
out.SetContinue(continueToken)
return out, nil
}
@@ -268,6 +268,58 @@ func TestListDashboardVersions(t *testing.T) {
}}}, res)
})
t.Run("List returns continue token when first fetch satisfies limit with more pages", func(t *testing.T) {
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
mockCli := new(client.MockK8sHandler)
dashboardVersionService.k8sclient = mockCli
dashboardVersionService.features = featuremgmt.WithFeatures()
dashboardService.On("GetDashboardUIDByID", mock.Anything,
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
query := dashver.ListDashboardVersionsQuery{DashboardID: 42, Limit: 2}
mockCli.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
firstPage := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"resourceVersion": "11",
"generation": int64(4),
"labels": map[string]any{
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
},
},
"spec": map[string]any{},
}},
{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"resourceVersion": "12",
"generation": int64(5),
"labels": map[string]any{
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
},
},
"spec": map[string]any{},
}},
},
}
firstMeta, err := meta.ListAccessor(firstPage)
require.NoError(t, err)
firstMeta.SetContinue("t1") // More pages exist
mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(firstPage, nil).Once()
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 2, len(res.Versions))
require.Equal(t, "t1", res.ContinueToken) // Token from first fetch when limit is satisfied
mockCli.AssertNumberOfCalls(t, "List", 1) // Only one fetch needed
})
t.Run("List returns correct continue token across multiple pages", func(t *testing.T) {
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
@@ -333,7 +385,79 @@ func TestListDashboardVersions(t *testing.T) {
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 3, len(res.Versions))
require.Equal(t, "t1", res.ContinueToken) // Implementation returns continue token from first page
require.Equal(t, "", res.ContinueToken) // Should return token from last fetch (empty = no more pages)
mockCli.AssertNumberOfCalls(t, "List", 2)
})
t.Run("List returns continue token from last fetch when more pages exist", func(t *testing.T) {
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
mockCli := new(client.MockK8sHandler)
dashboardVersionService.k8sclient = mockCli
dashboardVersionService.features = featuremgmt.WithFeatures()
dashboardService.On("GetDashboardUIDByID", mock.Anything,
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
query := dashver.ListDashboardVersionsQuery{DashboardID: 42, Limit: 3}
mockCli.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
firstPage := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"resourceVersion": "11",
"generation": int64(4),
"labels": map[string]any{
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
},
},
"spec": map[string]any{},
}},
{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"resourceVersion": "12",
"generation": int64(5),
"labels": map[string]any{
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
},
},
"spec": map[string]any{},
}},
},
}
firstMeta, err := meta.ListAccessor(firstPage)
require.NoError(t, err)
firstMeta.SetContinue("t1")
secondPage := &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"resourceVersion": "13",
"generation": int64(6),
"labels": map[string]any{
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
},
},
"spec": map[string]any{},
}},
},
}
secondMeta, err := meta.ListAccessor(secondPage)
require.NoError(t, err)
secondMeta.SetContinue("t2") // More pages exist
mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(firstPage, nil).Once()
mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(secondPage, nil).Once()
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 3, len(res.Versions))
require.Equal(t, "t2", res.ContinueToken) // Must return token from LAST fetch, not first
mockCli.AssertNumberOfCalls(t, "List", 2)
})
+15 -7
View File
@@ -494,13 +494,6 @@ var (
Owner: grafanaDatasourcesCoreServicesSquad,
FrontendOnly: true, // and can change at startup
},
{
Name: "queryServiceFromExplore",
Description: "Routes explore requests to the new query service",
Stage: FeatureStageExperimental,
Owner: grafanaDatasourcesCoreServicesSquad,
FrontendOnly: true,
},
{
Name: "cloudWatchBatchQueries",
Description: "Runs CloudWatch metrics queries as separate batches",
@@ -1953,6 +1946,14 @@ var (
Owner: identityAccessTeam,
Expression: "true",
},
{
Name: "pluginInsights",
Description: "Show insights for plugins in the plugin details page",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaPluginsPlatformSquad,
Expression: "false",
},
{
Name: "panelTimeSettings",
Description: "Enables a new panel time settings drawer",
@@ -1962,6 +1963,13 @@ var (
RequiresRestart: false,
HideFromDocs: false,
},
{
Name: "elasticsearchRawDSLQuery",
Description: "Enables the raw DSL query editor in the Elasticsearch data source",
Stage: FeatureStageExperimental,
Owner: grafanaPartnerPluginsSquad,
Expression: "false",
},
{
Name: "kubernetesAnnotations",
Description: "Enables app platform API for annotations",
+2 -1
View File
@@ -68,7 +68,6 @@ queryService,experimental,@grafana/grafana-datasources-core-services,false,true,
queryServiceWithConnections,experimental,@grafana/grafana-datasources-core-services,false,true,false
queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false
queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true
queryServiceFromExplore,experimental,@grafana/grafana-datasources-core-services,false,false,true
cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false
cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false
alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false
@@ -265,7 +264,9 @@ jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false
pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false
newPanelPadding,preview,@grafana/dashboards-squad,false,false,true
onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false
pluginInsights,experimental,@grafana/plugins-platform-backend,false,false,true
panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false
elasticsearchRawDSLQuery,experimental,@grafana/partner-datasources,false,false,false
kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false
awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false
transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
68 queryServiceWithConnections experimental @grafana/grafana-datasources-core-services false true false
69 queryServiceRewrite experimental @grafana/grafana-datasources-core-services false true false
70 queryServiceFromUI experimental @grafana/grafana-datasources-core-services false false true
queryServiceFromExplore experimental @grafana/grafana-datasources-core-services false false true
71 cloudWatchBatchQueries preview @grafana/aws-datasources false false false
72 cachingOptimizeSerializationMemoryUsage experimental @grafana/grafana-operator-experience-squad false false false
73 alertmanagerRemoteSecondary experimental @grafana/alerting-squad false false false
264 pluginStoreServiceLoading experimental @grafana/plugins-platform-backend false false false
265 newPanelPadding preview @grafana/dashboards-squad false false true
266 onlyStoreActionSets GA @grafana/identity-access-team false false false
267 pluginInsights experimental @grafana/plugins-platform-backend false false true
268 panelTimeSettings experimental @grafana/dashboards-squad false false false
269 elasticsearchRawDSLQuery experimental @grafana/partner-datasources false false false
270 kubernetesAnnotations experimental @grafana/grafana-backend-services-squad false false false
271 awsDatasourcesHttpProxy experimental @grafana/aws-datasources false false false
272 transformationsEmptyPlaceholder preview @grafana/datapro false false true
+4
View File
@@ -758,6 +758,10 @@ const (
// Enables a new panel time settings drawer
FlagPanelTimeSettings = "panelTimeSettings"
// FlagElasticsearchRawDSLQuery
// Enables the raw DSL query editor in the Elasticsearch data source
FlagElasticsearchRawDSLQuery = "elasticsearchRawDSLQuery"
// FlagKubernetesAnnotations
// Enables app platform API for annotations
FlagKubernetesAnnotations = "kubernetesAnnotations"
+29 -1
View File
@@ -1206,6 +1206,19 @@
"codeowner": "@grafana/partner-datasources"
}
},
{
"metadata": {
"name": "elasticsearchRawDSLQuery",
"resourceVersion": "1763508396079",
"creationTimestamp": "2025-11-18T23:26:36Z"
},
"spec": {
"description": "Enables the raw DSL query editor in the Elasticsearch data source",
"stage": "experimental",
"codeowner": "@grafana/partner-datasources",
"expression": "false"
}
},
{
"metadata": {
"name": "enableAppChromeExtensions",
@@ -2654,6 +2667,20 @@
"expression": "false"
}
},
{
"metadata": {
"name": "pluginInsights",
"resourceVersion": "1761300628147",
"creationTimestamp": "2025-10-24T10:10:28Z"
},
"spec": {
"description": "Show insights for plugins in the plugin details page",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend",
"frontend": true,
"expression": "false"
}
},
{
"metadata": {
"name": "pluginInstallAPISync",
@@ -2884,7 +2911,8 @@
"metadata": {
"name": "queryServiceFromExplore",
"resourceVersion": "1764664939750",
"creationTimestamp": "2025-04-02T10:00:33Z"
"creationTimestamp": "2025-04-02T10:00:33Z",
"deletionTimestamp": "2025-12-10T20:33:21Z"
},
"spec": {
"description": "Routes explore requests to the new query service",
+1 -1
View File
@@ -134,7 +134,7 @@ func (s *frontendService) addMiddlewares(m *web.Mux) {
loggermiddleware := loggermw.Provide(s.cfg, s.features)
m.Use(requestmeta.SetupRequestMetadata())
m.Use(middleware.RequestTracing(s.tracer, middleware.TraceAllPaths))
m.Use(middleware.RequestTracing(s.tracer, middleware.ShouldTraceAllPaths))
m.Use(middleware.RequestMetrics(s.features, s.cfg, s.promRegister))
m.UseMiddleware(s.contextMiddleware())
+3
View File
@@ -424,6 +424,9 @@ func (l *LibraryElementService) toLibraryElementError(err error, message string)
if errors.Is(err, model.ErrLibraryElementUIDTooLong) {
return response.Error(http.StatusBadRequest, model.ErrLibraryElementUIDTooLong.Error(), err)
}
if errors.Is(err, model.ErrLibraryElementProvisionedFolder) {
return response.Error(http.StatusConflict, model.ErrLibraryElementProvisionedFolder.Error(), err)
}
if err != nil && strings.Contains(err.Error(), "insufficient permissions") {
return response.Error(http.StatusForbidden, err.Error(), err)
}
+30
View File
@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/metrics"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -125,6 +126,20 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser id
}
}
if cmd.FolderUID != nil {
f, err := l.folderService.Get(c, &folder.GetFolderQuery{
OrgID: signedInUser.GetOrgID(),
UID: cmd.FolderUID,
SignedInUser: signedInUser,
})
if err != nil {
return model.LibraryElementDTO{}, err
}
if f.ManagedBy == utils.ManagerKindRepo {
return model.LibraryElementDTO{}, model.ErrLibraryElementProvisionedFolder
}
}
updatedModel := cmd.Model
var err error
if cmd.Kind == int64(model.PanelElement) {
@@ -601,6 +616,21 @@ func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInU
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
return model.LibraryElementDTO{}, err
}
if cmd.FolderUID != nil {
f, err := l.folderService.Get(c, &folder.GetFolderQuery{
OrgID: signedInUser.GetOrgID(),
UID: cmd.FolderUID,
SignedInUser: signedInUser,
})
if err != nil {
return model.LibraryElementDTO{}, err
}
if f.ManagedBy == utils.ManagerKindRepo {
return model.LibraryElementDTO{}, model.ErrLibraryElementProvisionedFolder
}
}
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
elementInDB, err := l.GetLibraryElement(c, signedInUser, session, uid)
if err != nil {
@@ -161,6 +161,8 @@ var (
ErrLibraryElementInvalidUID = errors.New("uid contains illegal characters")
// errLibraryElementUIDTooLong is an error for when the uid of a library element is invalid
ErrLibraryElementUIDTooLong = errors.New("uid too long, max 40 characters")
// ErrLibraryElementProvisionedFolder indicates that a library element cannot be created on a provisioned folder.
ErrLibraryElementProvisionedFolder = errors.New("resource type not supported in repository-managed folders")
)
// Commands
+29 -48
View File
@@ -6,10 +6,11 @@ import (
"fmt"
"strings"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/live/model"
)
@@ -25,17 +26,17 @@ const (
// DashboardEvent events related to dashboards
type dashboardEvent struct {
UID string `json:"uid"`
Action actionType `json:"action"` // saved, editing, deleted
SessionID string `json:"sessionId,omitempty"`
UID string `json:"uid"`
Action actionType `json:"action"` // saved, editing, deleted
SessionID string `json:"sessionId,omitempty"`
ResourceVersion string `json:"rv,omitempty"`
}
// DashboardHandler manages all the `grafana/dashboard/*` channels
type DashboardHandler struct {
Publisher model.ChannelPublisher
ClientCount model.ChannelClientCount
DashboardService dashboards.DashboardService
AccessControl accesscontrol.AccessControl
Publisher model.ChannelPublisher
ClientCount model.ChannelClientCount
AccessControl dashboards.DashboardAccessService
}
// GetHandlerForPath called on init
@@ -49,23 +50,15 @@ func (h *DashboardHandler) OnSubscribe(ctx context.Context, user identity.Reques
// make sure can view this dashboard
if len(parts) == 2 && parts[0] == "uid" {
query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.GetOrgID()}
_, err := h.DashboardService.GetDashboard(ctx, &query)
if err != nil {
logger.Error("Error getting dashboard", "query", query, "error", err)
return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
ns := types.OrgNamespaceFormatter(user.GetOrgID())
ok, err := h.AccessControl.HasDashboardAccess(ctx, user, utils.VerbGet, ns, parts[1])
if ok && err == nil {
return model.SubscribeReply{
Presence: true,
JoinLeave: true,
}, backend.SubscribeStreamStatusOK, nil
}
evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1]))
canView, err := h.AccessControl.Evaluate(ctx, user, evaluator)
if err != nil || !canView {
return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err
}
return model.SubscribeReply{
Presence: true,
JoinLeave: true,
}, backend.SubscribeStreamStatusOK, nil
return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err
}
// Unknown path
@@ -88,29 +81,16 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, requester identity.Req
// just ignore the event
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???")
}
query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: requester.GetOrgID()}
_, err = h.DashboardService.GetDashboard(ctx, &query)
if err != nil {
logger.Error("Unknown dashboard", "query", query)
return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil
}
evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1]))
canEdit, err := h.AccessControl.Evaluate(ctx, requester, evaluator)
if err != nil {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
ns := types.OrgNamespaceFormatter(requester.GetOrgID())
ok, err := h.AccessControl.HasDashboardAccess(ctx, requester, utils.VerbUpdate, ns, parts[1])
if ok && err == nil {
msg, err := json.Marshal(event)
if err != nil {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
}
return model.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil
}
// Ignore edit events if the user can not edit
if !canEdit {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil // NOOP
}
msg, err := json.Marshal(event)
if err != nil {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
}
return model.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil
}
return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil
@@ -126,10 +106,11 @@ func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error {
}
// DashboardSaved will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardSaved(orgID int64, uid string) error {
func (h *DashboardHandler) DashboardSaved(orgID int64, uid string, rv string) error {
return h.publish(orgID, dashboardEvent{
UID: uid,
Action: ActionSaved,
UID: uid,
Action: ActionSaved,
ResourceVersion: rv,
})
}
+11 -94
View File
@@ -27,13 +27,11 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -52,7 +50,6 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -72,28 +69,23 @@ type CoreGrafanaScope struct {
Dashboards DashboardActivityChannel
}
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
pluginStore pluginstore.Store, pluginClient plugins.Client, cacheService *localcache.CacheService,
dataSourceCache datasources.CacheService, secretsService secrets.Service,
func ProvideService(cfg *setting.Cfg, routeRegister routing.RouteRegister, plugCtxProvider *plugincontext.Provider,
pluginStore pluginstore.Store, pluginClient plugins.Client, dataSourceCache datasources.CacheService,
usageStatsService usagestats.Service, toggles featuremgmt.FeatureToggles,
accessControl accesscontrol.AccessControl, dashboardService dashboards.DashboardService,
orgService org.Service, configProvider apiserver.RestConfigProvider) (*GrafanaLive, error) {
dashboardService dashboards.DashboardAccessService,
configProvider apiserver.RestConfigProvider) (*GrafanaLive, error) {
g := &GrafanaLive{
Cfg: cfg,
Features: toggles,
PluginContextProvider: plugCtxProvider,
RouteRegister: routeRegister,
pluginStore: pluginStore,
pluginClient: pluginClient,
CacheService: cacheService,
DataSourceCache: dataSourceCache,
SecretsService: secretsService,
channels: make(map[string]model.ChannelHandler),
GrafanaScope: CoreGrafanaScope{
Features: make(map[string]model.ChannelHandlerFactory),
},
usageStatsService: usageStatsService,
orgService: orgService,
keyPrefix: "gf_live",
}
@@ -176,19 +168,13 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
// Initialize the main features
dash := &features.DashboardHandler{
Publisher: g.Publish,
ClientCount: g.ClientCount,
DashboardService: dashboardService,
AccessControl: accessControl,
Publisher: g.Publish,
ClientCount: g.ClientCount,
AccessControl: dashboardService,
}
g.GrafanaScope.Dashboards = dash
g.GrafanaScope.Features["dashboard"] = dash
// Testing watch with just the provisioning support -- this will be removed when it is well validated
//nolint:staticcheck // not yet migrated to OpenFeature
if toggles.IsEnabledGlobally(featuremgmt.FlagProvisioning) {
g.GrafanaScope.Features["watch"] = features.NewWatchRunner(g.Publish, configProvider)
}
g.GrafanaScope.Features["watch"] = features.NewWatchRunner(g.Publish, configProvider)
g.surveyCaller = survey.NewCaller(managedStreamRunner, node)
err = g.surveyCaller.SetupHandlers()
@@ -398,11 +384,11 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
pushPipelineWSHandler.ServeHTTP(ctx.Resp, r)
}
g.RouteRegister.Group("/api/live", func(group routing.RouteRegister) {
routeRegister.Group("/api/live", func(group routing.RouteRegister) {
group.Get("/ws", g.websocketHandler)
}, middleware.ReqSignedIn, requestmeta.SetSLOGroup(requestmeta.SLOGroupNone))
g.RouteRegister.Group("/api/live", func(group routing.RouteRegister) {
routeRegister.Group("/api/live", func(group routing.RouteRegister) {
group.Get("/push/:streamId", g.pushWebsocketHandler)
group.Get("/pipeline/push/*", g.pushPipelineWebsocketHandler)
}, middleware.ReqOrgAdmin, requestmeta.SetSLOGroup(requestmeta.SLOGroupNone))
@@ -461,13 +447,9 @@ type GrafanaLive struct {
PluginContextProvider *plugincontext.Provider
Cfg *setting.Cfg
Features featuremgmt.FeatureToggles
RouteRegister routing.RouteRegister
CacheService *localcache.CacheService
DataSourceCache datasources.CacheService
SecretsService secrets.Service
pluginStore pluginstore.Store
pluginClient plugins.Client
orgService org.Service
keyPrefix string // HA prefix for grafana cloud (since the org is always 1)
@@ -500,7 +482,7 @@ type GrafanaLive struct {
// DashboardActivityChannel is a service to advertise dashboard activity
type DashboardActivityChannel interface {
// Called when a dashboard is saved
DashboardSaved(orgID int64, uid string) error
DashboardSaved(orgID int64, uid string, rv string) error
// Called when a dashboard is deleted
DashboardDeleted(orgID int64, uid string) error
@@ -1356,71 +1338,6 @@ func (g *GrafanaLive) HandleWriteConfigsPostHTTP(c *contextmodel.ReqContext) res
})
}
// HandleWriteConfigsPutHTTP ...
func (g *GrafanaLive) HandleWriteConfigsPutHTTP(c *contextmodel.ReqContext) response.Response {
body, err := io.ReadAll(c.Req.Body)
if err != nil {
return response.Error(http.StatusInternalServerError, "Error reading body", err)
}
var cmd pipeline.WriteConfigUpdateCmd
err = json.Unmarshal(body, &cmd)
if err != nil {
return response.Error(http.StatusBadRequest, "Error decoding write config update command", err)
}
if cmd.UID == "" {
return response.Error(http.StatusBadRequest, "UID required", nil)
}
existingBackend, ok, err := g.pipelineStorage.GetWriteConfig(c.Req.Context(), c.GetOrgID(), pipeline.WriteConfigGetCmd{
UID: cmd.UID,
})
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to get write config", err)
}
if ok {
if cmd.SecureSettings == nil {
cmd.SecureSettings = map[string]string{}
}
secureJSONData, err := g.SecretsService.DecryptJsonData(c.Req.Context(), existingBackend.SecureSettings)
if err != nil {
logger.Error("Error decrypting secure settings", "error", err)
return response.Error(http.StatusInternalServerError, "Error decrypting secure settings", err)
}
for k, v := range secureJSONData {
if _, ok := cmd.SecureSettings[k]; !ok {
cmd.SecureSettings[k] = v
}
}
}
result, err := g.pipelineStorage.UpdateWriteConfig(c.Req.Context(), c.GetOrgID(), cmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to update write config", err)
}
return response.JSON(http.StatusOK, util.DynMap{
"writeConfig": pipeline.WriteConfigToDto(result),
})
}
// HandleWriteConfigsDeleteHTTP ...
func (g *GrafanaLive) HandleWriteConfigsDeleteHTTP(c *contextmodel.ReqContext) response.Response {
body, err := io.ReadAll(c.Req.Body)
if err != nil {
return response.Error(http.StatusInternalServerError, "Error reading body", err)
}
var cmd pipeline.WriteConfigDeleteCmd
err = json.Unmarshal(body, &cmd)
if err != nil {
return response.Error(http.StatusBadRequest, "Error decoding write config delete command", err)
}
if cmd.UID == "" {
return response.Error(http.StatusBadRequest, "UID required", nil)
}
err = g.pipelineStorage.DeleteWriteConfig(c.Req.Context(), c.GetOrgID(), cmd)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete write config", err)
}
return response.JSON(http.StatusOK, util.DynMap{})
}
// Write to the standard log15 logger
func handleLog(msg centrifuge.LogEntry) {
arr := make([]interface{}, 0)
+3 -6
View File
@@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
@@ -340,16 +339,14 @@ func setupLiveService(cfg *setting.Cfg, t *testing.T) (*GrafanaLive, error) {
cfg = setting.NewCfg()
}
return ProvideService(nil,
cfg,
return ProvideService(cfg,
routing.NewRouteRegister(),
nil, nil, nil, nil,
nil, nil, nil,
nil,
&usagestats.UsageStatsMock{T: t},
featuremgmt.WithFeatures(),
acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
&dashboards.FakeDashboardService{},
nil, nil)
nil)
}
type dummyTransport struct {
+2 -1
View File
@@ -289,12 +289,13 @@ var (
Role: accesscontrol.RoleDTO{
Name: accesscontrol.FixedRolePrefix + "alerting:admin",
DisplayName: "Full admin access",
Description: "Full write access in Grafana and all external providers, including their permissions and secrets",
Description: "Full write access in Grafana and all external providers, including their permissions, protected fields and secrets",
Group: models.AlertRolesGroup,
Permissions: accesscontrol.ConcatPermissions(alertingWriterRole.Role.Permissions, []accesscontrol.Permission{
{Action: accesscontrol.ActionAlertingReceiversPermissionsRead, Scope: models.ScopeReceiversAll},
{Action: accesscontrol.ActionAlertingReceiversPermissionsWrite, Scope: models.ScopeReceiversAll},
{Action: accesscontrol.ActionAlertingReceiversReadSecrets, Scope: models.ScopeReceiversAll},
{Action: accesscontrol.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversAll},
}),
},
Grants: []string{string(org.RoleAdmin)},
@@ -95,6 +95,26 @@ var (
)
}
// Asserts pre-conditions for access to modify protected fields of receivers. If this evaluates to false, the user cannot modify protected fields of any receivers.
updateReceiversProtectedPreConditionsEval = ac.EvalAll(
updateReceiversPreConditionsEval,
ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected), // Action for receivers. UID scope.
)
// Asserts access to modify protected fields of a specific receiver.
updateReceiverProtectedEval = func(uid string) ac.Evaluator {
return ac.EvalAll(
updateReceiverEval(uid),
ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected, models.ScopeReceiversProvider.GetResourceScopeUID(uid)),
)
}
// Asserts access to modify protected fields of all receivers.
updateAllReceiverProtectedEval = ac.EvalAll(
updateAllReceiversEval,
ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected, models.ScopeReceiversAll),
)
// Delete
// Asserts pre-conditions for delete access to receivers. If this evaluates to false, the user cannot delete any receivers.
@@ -141,12 +161,13 @@ var (
)
type ReceiverAccess[T models.Identified] struct {
read actionAccess[T]
readDecrypted actionAccess[T]
create actionAccess[T]
update actionAccess[T]
delete actionAccess[T]
permissions actionAccess[T]
read actionAccess[T]
readDecrypted actionAccess[T]
create actionAccess[T]
update actionAccess[T]
updateProtected actionAccess[T]
delete actionAccess[T]
permissions actionAccess[T]
}
// NewReceiverAccess creates a new ReceiverAccess service. If includeProvisioningActions is true, the service will include
@@ -201,6 +222,18 @@ func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvision
},
authorizeAll: updateAllReceiversEval,
},
updateProtected: actionAccess[T]{
genericService: genericService{
ac: a,
},
resource: "receiver",
action: "update protected fields of", // this produces message "user is not authorized to update protected fields of X receiver"
authorizeSome: updateReceiversProtectedPreConditionsEval,
authorizeOne: func(receiver models.Identified) ac.Evaluator {
return updateReceiverProtectedEval(receiver.GetUID())
},
authorizeAll: updateAllReceiverProtectedEval,
},
delete: actionAccess[T]{
genericService: genericService{
ac: a,
@@ -311,6 +344,14 @@ func (s ReceiverAccess[T]) AuthorizeUpdate(ctx context.Context, user identity.Re
return s.update.Authorize(ctx, user, receiver)
}
func (s ReceiverAccess[T]) HasUpdateProtected(ctx context.Context, user identity.Requester, receiver T) (bool, error) {
return s.updateProtected.Has(ctx, user, receiver)
}
func (s ReceiverAccess[T]) AuthorizeUpdateProtected(ctx context.Context, user identity.Requester, receiver T) error {
return s.updateProtected.Authorize(ctx, user, receiver)
}
// Global
// AuthorizeCreate checks if user has access to create receivers. Returns an error if user does not have access.
@@ -380,6 +421,12 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester,
basePerms.Set(models.ReceiverPermissionDelete, true) // Has access to all receivers.
}
if err := s.updateProtected.AuthorizePreConditions(ctx, user); err != nil {
basePerms.Set(models.ReceiverPermissionModifyProtected, false)
} else if err := s.updateProtected.AuthorizeAll(ctx, user); err == nil {
basePerms.Set(models.ReceiverPermissionModifyProtected, true)
}
if basePerms.AllSet() {
// Shortcut for the case when all permissions are known based on preconditions.
result := make(map[string]models.ReceiverPermissionSet, len(receivers))
@@ -412,6 +459,11 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester,
permSet.Set(models.ReceiverPermissionDelete, err == nil)
}
if _, ok := permSet.Has(models.ReceiverPermissionModifyProtected); !ok {
err := s.updateProtected.authorize(ctx, user, rcv)
permSet.Set(models.ReceiverPermissionModifyProtected, err == nil)
}
result[rcv.GetUID()] = permSet
}
return result, nil
@@ -204,6 +204,33 @@ func TestReceiverAccess(t *testing.T) {
recv3.UID: permissions(),
},
},
{
name: "update protected cannot update receivers",
user: newEmptyUser(
ac.Permission{Action: ac.ActionAlertingReceiversRead, Scope: models.ScopeReceiversAll},
ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversAll},
),
expected: map[string]models.ReceiverPermissionSet{
recv1.UID: permissions(),
recv2.UID: permissions(),
recv3.UID: permissions(),
},
},
{
name: "update protected receivers",
user: newEmptyUser(
ac.Permission{Action: ac.ActionAlertingReceiversRead, Scope: models.ScopeReceiversAll},
ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)},
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)},
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)},
ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)},
),
expected: map[string]models.ReceiverPermissionSet{
recv1.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionModifyProtected),
recv2.UID: permissions(models.ReceiverPermissionWrite),
recv3.UID: permissions(),
},
},
// Receiver delete.
{
name: "global receiver delete should have delete but no write",
+9 -1
View File
@@ -10,8 +10,10 @@ import (
"time"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/alerting/receivers/schema"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -32,6 +34,7 @@ const (
type receiversAuthz interface {
FilterRead(ctx context.Context, user identity.Requester, receivers ...ReceiverStatus) ([]ReceiverStatus, error)
AuthorizeUpdateProtected(context.Context, identity.Requester, ReceiverStatus) error
}
type AlertmanagerSrv struct {
@@ -210,11 +213,16 @@ func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) respons
}
func (srv AlertmanagerSrv) RoutePostTestReceivers(c *contextmodel.ReqContext, body apimodels.TestReceiversConfigBodyParams) response.Response {
if err := srv.crypto.ProcessSecureSettings(c.Req.Context(), c.GetOrgID(), body.Receivers); err != nil {
if err := srv.crypto.ProcessSecureSettings(c.Req.Context(), c.GetOrgID(), body.Receivers, func(receiverName string, paths []schema.IntegrationFieldPath) error {
return srv.receiverAuthz.AuthorizeUpdateProtected(c.Req.Context(), c.SignedInUser, ReceiverStatus{Name: receiverName})
}); err != nil {
var unknownReceiverError UnknownReceiverError
if errors.As(err, &unknownReceiverError) {
return ErrResp(http.StatusBadRequest, err, "")
}
if errors.As(err, &errutil.Error{}) {
return response.Err(err)
}
return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration")
}
@@ -2886,6 +2886,189 @@ func TestRouteGetRuleStatuses(t *testing.T) {
})
}
})
t.Run("with rule_matcher filter", func(t *testing.T) {
fakeStore, fakeAIM, api := setupAPI(t)
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule1"), gen.WithLabels(map[string]string{"team": "alerting", "severity": "critical"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule2"), gen.WithLabels(map[string]string{"team": "Alerting", "severity": "warning"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule3"), gen.WithLabels(map[string]string{"team": "platform", "severity": "critical"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule4"), gen.WithLabels(map[string]string{"env": "production"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_special"), gen.WithLabels(map[string]string{"key": `value"with"quotes`}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_empty"), gen.WithLabels(map[string]string{"empty": ""}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_nonempty"), gen.WithLabels(map[string]string{"empty": "nonempty"}), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule_multiline"), gen.WithLabels(map[string]string{"description": "line1\nline2\\end\"quote"}), gen.WithNoNotificationSettings())
testCases := []struct {
name string
matchers []string
expectedUIDs []string
}{
{
name: "equality matcher filters by team=alerting",
matchers: []string{`{"name":"team","value":"alerting","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule1"},
},
{
name: "inequality matcher filters severity!=warning",
matchers: []string{`{"name":"severity","value":"warning","isRegex":false,"isEqual":false}`},
expectedUIDs: []string{"rule1", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
{
name: "regex matcher filters team=~plat.*",
matchers: []string{`{"name":"team","value":"plat.*","isRegex":true,"isEqual":true}`},
expectedUIDs: []string{"rule3"},
},
{
name: "not-regex matcher filters severity!~warn.*",
matchers: []string{`{"name":"severity","value":"warn.*","isRegex":true,"isEqual":false}`},
expectedUIDs: []string{"rule1", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
{
name: "multiple matchers are ANDed",
matchers: []string{
`{"name":"team","value":"alerting","isRegex":false,"isEqual":true}`,
`{"name":"severity","value":"critical","isRegex":false,"isEqual":true}`,
},
expectedUIDs: []string{"rule1"},
},
{
name: "matcher with non-existent label returns no rules",
matchers: []string{`{"name":"nonexistent","value":"value","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{},
},
{
name: "equality matcher is case-sensitive",
matchers: []string{`{"name":"team","value":"Alerting","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule2"},
},
{
name: "quotes in label value are handled correctly",
matchers: []string{`{"name":"key","value":"value\"with\"quotes","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule_special"},
},
{
name: "no matchers returns all rules",
matchers: []string{},
expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
{
name: "empty string value matches correctly",
matchers: []string{`{"name":"empty","value":"","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_multiline"},
},
{
name: "special characters in label value are handled correctly",
matchers: []string{`{"name":"description","value":"line1\nline2\\end\"quote","isRegex":false,"isEqual":true}`},
expectedUIDs: []string{"rule_multiline"},
},
{
name: "inequality matcher on non-existent label matches all rules",
matchers: []string{`{"name":"nonexistent","value":"value","isRegex":false,"isEqual":false}`},
expectedUIDs: []string{"rule1", "rule2", "rule3", "rule4", "rule_special", "rule_empty", "rule_nonempty", "rule_multiline"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
reqURL := "/api/v1/rules"
for i, matcher := range tc.matchers {
if i == 0 {
reqURL += "?rule_matcher=" + url.QueryEscape(matcher)
} else {
reqURL += "&rule_matcher=" + url.QueryEscape(matcher)
}
}
req, err := http.NewRequest("GET", reqURL, nil)
require.NoError(t, err)
ctx := &contextmodel.ReqContext{
Context: &web.Context{Req: req},
SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions},
}
resp := api.RouteGetRuleStatuses(ctx)
require.Equal(t, http.StatusOK, resp.Status())
var res apimodels.RuleResponse
require.NoError(t, json.Unmarshal(resp.Body(), &res))
require.Equal(t, "success", res.Status)
actualUIDs := []string{}
for _, group := range res.Data.RuleGroups {
for _, rule := range group.Rules {
actualUIDs = append(actualUIDs, rule.UID)
}
}
require.ElementsMatch(t, tc.expectedUIDs, actualUIDs)
})
}
})
t.Run("pagination with rule_matcher in-memory filtering", func(t *testing.T) {
fakeStore, fakeAIM, api := setupAPI(t)
// Create 3 groups with 2 rules each:
// Group 1 & 2: team=backend (won't match filter)
// Group 3: team=frontend (will match filter)
// This tests that pagination continues fetching when early pages are filtered out
group1Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace1", RuleGroup: "group1"}
group2Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace2", RuleGroup: "group2"}
group3Key := ngmodels.AlertRuleGroupKey{OrgID: orgID, NamespaceUID: "namespace3", RuleGroup: "group3"}
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule1"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group1Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule2"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group1Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule3"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group2Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule4"), gen.WithLabels(map[string]string{"team": "security"}), gen.WithGroupKey(group2Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule5"), gen.WithLabels(map[string]string{"team": "alerting"}), gen.WithGroupKey(group3Key), gen.WithNoNotificationSettings())
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery(),
gen.WithUID("rule6"), gen.WithLabels(map[string]string{"team": "alerting"}), gen.WithGroupKey(group3Key), gen.WithNoNotificationSettings())
// Request with regex rule_matcher filter for team=~"alerting" and group_limit=1 to force pagination
matcher := `{"name":"team","value":"alerting","isRegex":true,"isEqual":true}`
reqURL := "/api/v1/rules?rule_matcher=" + url.QueryEscape(matcher) + "&group_limit=1"
req, err := http.NewRequest("GET", reqURL, nil)
require.NoError(t, err)
ctx := &contextmodel.ReqContext{
Context: &web.Context{Req: req},
SignedInUser: &user.SignedInUser{OrgID: orgID, Permissions: queryPermissions},
}
resp := api.RouteGetRuleStatuses(ctx)
require.Equal(t, http.StatusOK, resp.Status())
var res apimodels.RuleResponse
require.NoError(t, json.Unmarshal(resp.Body(), &res))
require.Equal(t, "success", res.Status)
actualUIDs := []string{}
for _, group := range res.Data.RuleGroups {
for _, rule := range group.Rules {
actualUIDs = append(actualUIDs, rule.UID)
}
}
// Should return group3 rules (rule5, rule6), pagination should continue past filtered groups
require.ElementsMatch(t, []string{"rule5", "rule6"}, actualUIDs)
})
}
func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) {
@@ -33,6 +33,12 @@ import (
"go.opentelemetry.io/otel/trace"
)
const (
queryIncludeInternalLabels = "includeInternalLabels"
queryRuleMatcher = "rule_matcher"
queryInstanceMatcher = "matcher"
)
type RuleStoreReader interface {
GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error)
ListAlertRulesStoreV2
@@ -62,6 +68,20 @@ type PrometheusSrv struct {
// Package-level OpenTelemetry tracer per Grafana instrumentation conventions.
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/ngalert/api/prometheus")
// badRequestError returns a Prometheus-compatible error response for bad request data.
func badRequestError(err error) apimodels.RuleResponse {
return apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
Status: "error",
Error: err.Error(),
ErrorType: apiv1.ErrBadData,
},
Data: apimodels.RuleDiscovery{
RuleGroups: []apimodels.RuleGroup{},
},
}
}
func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store RuleStoreReader, authz RuleGroupAccessControlService, provenanceStore ProvenanceStore) *PrometheusSrv {
return &PrometheusSrv{
log,
@@ -73,8 +93,6 @@ func NewPrometheusSrv(log log.Logger, manager state.AlertInstanceManager, status
}
}
const queryIncludeInternalLabels = "includeInternalLabels"
func getBoolWithDefault(vals url.Values, field string, d bool) bool {
f := vals.Get(field)
if f == "" {
@@ -188,15 +206,15 @@ func getPanelIDFromQuery(v url.Values) (int64, error) {
return 0, nil
}
func getMatchersFromQuery(v url.Values) (labels.Matchers, error) {
func getMatchersFromQuery(v url.Values, paramName string) (labels.Matchers, error) {
var matchers labels.Matchers
for _, s := range v["matcher"] {
for _, s := range v[paramName] {
var m labels.Matcher
if err := json.Unmarshal([]byte(s), &m); err != nil {
return nil, err
}
if len(m.Name) == 0 {
return nil, errors.New("bad matcher: the name cannot be blank")
return nil, fmt.Errorf("bad %s: the name cannot be blank", paramName)
}
matchers = append(matchers, &m)
}
@@ -454,9 +472,11 @@ type paginationContext struct {
stateFilterSet map[eval.State]struct{}
healthFilterSet map[string]struct{}
matchers labels.Matchers
ruleLabelMatchers labels.Matchers
labelOptions []ngmodels.LabelOption
limitAlertsPerRule int64
limitRulesPerGroup int64
compact bool
}
// pageResult is the result of fetching and filtering of one page
@@ -475,6 +495,9 @@ func accumulateTotals(dest, source map[string]int64) {
// fetchAndFilterPage fetches one page from the store and applies filters
func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlertRulesStoreV2, span trace.Span, token string, remainingGroups, remainingRules int64) (pageResult, error) {
// Split matchers: only equality/inequality are supported by the store
storeMatchers := filterOutRegexMatchers(ctx.ruleLabelMatchers)
byGroupQuery := ngmodels.ListAlertRulesExtendedQuery{
ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{
OrgID: ctx.opts.OrgID,
@@ -487,11 +510,13 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
DataSourceUIDs: ctx.dataSourceUIDs,
SearchTitle: ctx.title,
SearchRuleGroup: ctx.searchRuleGroup,
LabelMatchers: storeMatchers,
},
RuleType: ctx.ruleType,
Limit: remainingGroups,
RuleLimit: remainingRules,
ContinueToken: token,
Compact: ctx.compact,
}
ruleList, newToken, err := store.ListAlertRulesByGroup(ctx.opts.Ctx, &byGroupQuery)
@@ -519,7 +544,7 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
log, rg.GroupKey, rg.Folder, rg.Rules,
ctx.provenanceRecords, ctx.limitAlertsPerRule,
ctx.stateFilterSet, ctx.matchers, ctx.labelOptions,
ctx.ruleStatusMutator, ctx.alertStateMutator,
ctx.ruleStatusMutator, ctx.alertStateMutator, ctx.compact,
)
ruleGroup.Totals = totals
accumulateTotals(result.totalsDelta, totals)
@@ -532,6 +557,8 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
filterRulesByHealth(ruleGroup, ctx.healthFilterSet)
}
filterRulesByLabelMatchers(ruleGroup, ctx.ruleLabelMatchers)
if ctx.limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > ctx.limitRulesPerGroup {
ruleGroup.Rules = ruleGroup.Rules[0:ctx.limitRulesPerGroup]
}
@@ -544,6 +571,17 @@ func (ctx *paginationContext) fetchAndFilterPage(log log.Logger, store ListAlert
return result, nil
}
func filterOutRegexMatchers(matchers labels.Matchers) labels.Matchers {
var result labels.Matchers
for _, m := range matchers {
if m.Type == labels.MatchEqual || m.Type == labels.MatchNotEqual {
result = append(result, m)
}
}
return result
}
// paginateRuleGroups fetches pages until limits are satisfied applying filters at each step
func paginateRuleGroups(log log.Logger, store ListAlertRulesStoreV2, ctx *paginationContext, span trace.Span, maxGroups, maxRules int64, startToken string) ([]apimodels.RuleGroup, map[string]int64, string, error) {
allGroups := []apimodels.RuleGroup{}
@@ -642,21 +680,30 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
attribute.Int64("limit_rules", limitRulesPerGroup),
attribute.Int64("limit_alerts", limitAlertsPerRule),
)
matchers, err := getMatchersFromQuery(opts.Query)
matchers, err := getMatchersFromQuery(opts.Query, queryInstanceMatcher)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
span.SetAttributes(attribute.Int("matcher_count", len(matchers)))
ruleLabelMatchers, err := getMatchersFromQuery(opts.Query, queryRuleMatcher)
if err != nil {
return badRequestError(err)
}
regexCount := 0
for _, m := range ruleLabelMatchers {
if m.Type == labels.MatchRegexp || m.Type == labels.MatchNotRegexp {
regexCount++
}
}
span.SetAttributes(
attribute.Int("rule_matcher_count", len(ruleLabelMatchers)),
attribute.Int("rule_matcher_regex_count", regexCount),
)
stateFilterSet, err := GetStatesFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
span.SetAttributes(
attribute.Int("state_filter_count", len(stateFilterSet)),
@@ -665,10 +712,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
healthFilterSet, err := GetHealthFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
span.SetAttributes(
attribute.Int("health_filter_count", len(healthFilterSet)),
@@ -785,6 +829,8 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
}
span.SetAttributes(attribute.Int("rule_name_count", len(ruleNamesSet)))
compact := getBoolWithDefault(opts.Query, "compact", false)
span.SetAttributes(attribute.Bool("compact", compact))
pagCtx := &paginationContext{
opts: opts,
provenanceRecords: provenanceRecords,
@@ -804,9 +850,11 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
stateFilterSet: stateFilterSet,
healthFilterSet: healthFilterSet,
matchers: matchers,
ruleLabelMatchers: ruleLabelMatchers,
labelOptions: labelOptions,
limitAlertsPerRule: limitAlertsPerRule,
limitRulesPerGroup: limitRulesPerGroup,
compact: compact,
}
groups, rulesTotals, continueToken, err := paginateRuleGroups(log, store, pagCtx, span, maxGroups, maxRules, nextToken)
@@ -828,6 +876,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt
return ruleResponse
}
// nolint:gocyclo
func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts RuleGroupStatusesOptions, ruleStatusMutator RuleStatusMutator, alertStateMutator RuleAlertStateMutator, provenanceRecords map[string]ngmodels.Provenance) apimodels.RuleResponse {
ruleResponse := apimodels.RuleResponse{
DiscoveryBase: apimodels.DiscoveryBase{
@@ -841,41 +890,30 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
dashboardUID := opts.Query.Get("dashboard_uid")
panelID, err := getPanelIDFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = fmt.Sprintf("invalid panel_id: %s", err.Error())
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(fmt.Errorf("invalid panel_id: %w", err))
}
if dashboardUID == "" && panelID != 0 {
ruleResponse.Status = "error"
ruleResponse.Error = "panel_id must be set with dashboard_uid"
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(errors.New("panel_id must be set with dashboard_uid"))
}
limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1)
limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1)
matchers, err := getMatchersFromQuery(opts.Query)
matchers, err := getMatchersFromQuery(opts.Query, queryInstanceMatcher)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
ruleLabelMatchers, err := getMatchersFromQuery(opts.Query, queryRuleMatcher)
if err != nil {
return badRequestError(err)
}
stateFilterSet, err := GetStatesFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
healthFilterSet, err := GetHealthFromQuery(opts.Query)
if err != nil {
ruleResponse.Status = "error"
ruleResponse.Error = err.Error()
ruleResponse.ErrorType = apiv1.ErrBadData
return ruleResponse
return badRequestError(err)
}
var labelOptions []ngmodels.LabelOption
@@ -908,6 +946,9 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
dataSourceUIDs := opts.Query["datasource_uid"]
searchRuleGroup := opts.Query.Get("search.rule_group")
// Split matchers: only equality/inequality are supported by the store
storeMatchers := filterOutRegexMatchers(ruleLabelMatchers)
alertRuleQuery := ngmodels.ListAlertRulesQuery{
OrgID: opts.OrgID,
NamespaceUIDs: namespaceUIDs,
@@ -919,6 +960,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
SearchTitle: title,
SearchRuleGroup: searchRuleGroup,
DataSourceUIDs: dataSourceUIDs,
LabelMatchers: storeMatchers,
}
ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery)
if err != nil {
@@ -959,7 +1001,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
break
}
ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator)
ruleGroup, totals := toRuleGroup(log, rg.GroupKey, rg.Folder, rg.Rules, provenanceRecords, limitAlertsPerRule, stateFilterSet, matchers, labelOptions, ruleStatusMutator, alertStateMutator, false)
ruleGroup.Totals = totals
for k, v := range totals {
rulesTotals[k] += v
@@ -973,6 +1015,10 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru
filterRulesByHealth(ruleGroup, healthFilterSet)
}
if len(ruleLabelMatchers) > 0 {
filterRulesByLabelMatchers(ruleGroup, ruleLabelMatchers)
}
if limitRulesPerGroup > -1 && int64(len(ruleGroup.Rules)) > limitRulesPerGroup {
ruleGroup.Rules = ruleGroup.Rules[0:limitRulesPerGroup]
}
@@ -1100,6 +1146,30 @@ func filterRulesByHealth(ruleGroup *apimodels.RuleGroup, withHealthFast map[stri
ruleGroup.Rules = filteredRules
}
func filterRulesByLabelMatchers(ruleGroup *apimodels.RuleGroup, matchers labels.Matchers) {
if len(matchers) == 0 {
return
}
filteredRules := make([]apimodels.AlertingRule, 0, len(ruleGroup.Rules))
for _, rule := range ruleGroup.Rules {
ruleLabels := rule.Labels.Map()
matches := true
for _, m := range matchers {
if !m.Matches(ruleLabels[m.Name]) {
matches = false
break
}
}
if matches {
filteredRules = append(filteredRules, rule)
}
}
ruleGroup.Rules = filteredRules
}
// This is the same as matchers.Matches but avoids the need to create a LabelSet
func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool {
for _, m := range matchers {
@@ -1110,7 +1180,7 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool {
return true
}
func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, limitAlerts int64, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, ruleStatusMutator RuleStatusMutator, ruleAlertStateMutator RuleAlertStateMutator) (*apimodels.RuleGroup, map[string]int64) {
func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, provenanceRecords map[string]ngmodels.Provenance, limitAlerts int64, stateFilterSet map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption, ruleStatusMutator RuleStatusMutator, ruleAlertStateMutator RuleAlertStateMutator, compact bool) (*apimodels.RuleGroup, map[string]int64) {
newGroup := &apimodels.RuleGroup{
Name: groupKey.RuleGroup,
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana.
@@ -1126,10 +1196,14 @@ func toRuleGroup(log log.Logger, groupKey ngmodels.AlertRuleGroupKey, folderFull
if prov, exists := provenanceRecords[rule.ResourceID()]; exists {
provenance = prov
}
var query string
if !compact {
query = ruleToQuery(log, rule)
}
alertingRule := apimodels.AlertingRule{
State: "inactive",
Name: rule.Title,
Query: ruleToQuery(log, rule),
Query: query,
QueriedDatasourceUIDs: extractDatasourceUIDs(rule),
Duration: rule.For.Seconds(),
KeepFiringFor: rule.KeepFiringFor.Seconds(),
@@ -462,4 +462,10 @@ type GetGrafanaRuleStatusesParams struct {
// in: query
// required: false
Matchers []string `json:"matcher"`
// Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {"type":0,"name":"severity","value":"critical"}).
// For equality matchers with empty string values (e.g., name=""), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).
// in: query
// required: false
RuleLabelMatchers []string `json:"rule_matcher"`
}
@@ -7561,6 +7561,15 @@
},
"name": "matcher",
"type": "array"
},
{
"description": "Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}).\nFor equality matchers with empty string values (e.g., name=\"\"), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).",
"in": "query",
"items": {
"type": "string"
},
"name": "rule_matcher",
"type": "array"
}
],
"responses": {
@@ -1947,6 +1947,15 @@
"description": "Filter by label matchers encoded as JSON representations of Prometheus matchers (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}). Provide one matcher per query string value.",
"name": "matcher",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"description": "Filter rules by their static labels (not alert instance labels). Each value is a JSON-encoded Prometheus-like matcher (for example, {\"type\":0,\"name\":\"severity\",\"value\":\"critical\"}).\nFor equality matchers with empty string values (e.g., name=\"\"), rules that have the label with an empty value OR rules without the label will match (standard Prometheus behavior).",
"name": "rule_matcher",
"in": "query"
}
],
"responses": {
@@ -110,6 +110,12 @@ func (aq *AlertQuery) String() string {
}
func (aq *AlertQuery) setModelProps() error {
if aq.Model == nil {
// No data to extract, use an empty map.
aq.modelProps = map[string]any{}
return nil
}
aq.modelProps = make(map[string]any)
err := json.Unmarshal(aq.Model, &aq.modelProps)
if err != nil {
@@ -18,6 +18,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/prometheus/alertmanager/pkg/labels"
prommodels "github.com/prometheus/common/model"
"github.com/grafana/grafana-plugin-sdk-go/data"
@@ -1012,6 +1013,10 @@ type ListAlertRulesQuery struct {
SearchRuleGroup string
HasPrometheusRuleDefinition *bool
// LabelMatchers filters rules by their labels.
// Only equality and inequality matchers are supported, no regex operators.
LabelMatchers labels.Matchers
}
type ListAlertRulesExtendedQuery struct {
@@ -1022,6 +1027,7 @@ type ListAlertRulesExtendedQuery struct {
Limit int64
RuleLimit int64
ContinueToken string
Compact bool
}
// CountAlertRulesQuery is the query for counting alert rules
+6 -4
View File
@@ -9,10 +9,11 @@ import (
type ReceiverPermission string
const (
ReceiverPermissionReadSecret ReceiverPermission = "secrets"
ReceiverPermissionAdmin ReceiverPermission = "admin"
ReceiverPermissionWrite ReceiverPermission = "write"
ReceiverPermissionDelete ReceiverPermission = "delete"
ReceiverPermissionReadSecret ReceiverPermission = "secrets"
ReceiverPermissionAdmin ReceiverPermission = "admin"
ReceiverPermissionWrite ReceiverPermission = "write"
ReceiverPermissionDelete ReceiverPermission = "delete"
ReceiverPermissionModifyProtected ReceiverPermission = "modify-protected"
)
// ReceiverPermissions returns all possible silence permissions.
@@ -22,6 +23,7 @@ func ReceiverPermissions() []ReceiverPermission {
ReceiverPermissionAdmin,
ReceiverPermissionWrite,
ReceiverPermissionDelete,
ReceiverPermissionModifyProtected,
}
}
@@ -0,0 +1,230 @@
package models
import (
"fmt"
"reflect"
"strings"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/alerting/receivers/schema"
"github.com/grafana/grafana/pkg/util/cmputil"
)
type IntegrationDiffReport struct {
cmputil.DiffReport
}
// expandPaths recursively collects all sub-paths for keys in the provided map value
func (r IntegrationDiffReport) expandPaths(basePath schema.IntegrationFieldPath, mapVal reflect.Value) []schema.IntegrationFieldPath {
result := make([]schema.IntegrationFieldPath, 0)
iter := mapVal.MapRange()
for iter.Next() {
keyStr := fmt.Sprintf("%v", iter.Key()) // Assume string keys
p := basePath.With(keyStr)
// Recurse if the sub-value is another map
if m, ok := r.getMap(iter.Value()); ok {
result = append(result, r.expandPaths(p, m)...)
continue
}
result = append(result, p)
}
return result
}
func (r IntegrationDiffReport) getMap(v reflect.Value) (reflect.Value, bool) {
if v.Kind() == reflect.Map {
return v, true
}
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
return r.getMap(v.Elem())
}
return reflect.Value{}, false
}
func (r IntegrationDiffReport) needExpand(diff cmputil.Diff) (reflect.Value, bool) {
ml, lok := r.getMap(diff.Left)
mr, rok := r.getMap(diff.Right)
if lok == rok {
return reflect.Value{}, false
}
if lok {
return ml, true
}
return mr, true
}
func (r IntegrationDiffReport) GetSettingsPaths() []schema.IntegrationFieldPath {
diffs := r.GetDiffsForField("Settings")
paths := make([]schema.IntegrationFieldPath, 0, len(diffs))
for _, diff := range diffs {
// diff.Path has format like Settings[url] or Settings[sub-form][field]
p := diff.Path
var path schema.IntegrationFieldPath
for {
start := strings.Index(p, "[")
if start == -1 {
break
}
p = p[start+1:]
end := strings.Index(p, "]")
if end == -1 {
break
}
fieldName := p[:end]
p = p[end+1:]
path = append(path, fieldName)
}
if m, ok := r.needExpand(diff); ok {
paths = append(paths, r.expandPaths(path, m)...)
continue
}
if len(path) > 0 {
paths = append(paths, path)
}
}
return paths
}
func (r IntegrationDiffReport) GetSecureSettingsPaths() []schema.IntegrationFieldPath {
diffs := r.GetDiffsForField("SecureSettings")
paths := make([]schema.IntegrationFieldPath, 0, len(diffs))
for _, diff := range diffs {
if diff.Path == "SecureSettings" {
if m, ok := r.needExpand(diff); ok {
paths = append(paths, r.expandPaths(nil, m)...)
}
continue
}
// diff.Path has format like SecureSettings[field.sub-field.sub]
p := schema.ParseIntegrationPath(diff.Path[len("SecureSettings[") : len(diff.Path)-1])
paths = append(paths, p)
}
return paths
}
func (integration *Integration) Diff(incoming Integration) IntegrationDiffReport {
var reporter cmputil.DiffReporter
var settingsCmp = cmpopts.AcyclicTransformer("settingsMap", func(in map[string]any) map[string]any {
if in == nil {
return map[string]any{}
}
return in
})
var secureCmp = cmpopts.AcyclicTransformer("secureMap", func(in map[string]string) map[string]string {
if in == nil {
return map[string]string{}
}
return in
})
schemaCmp := cmp.Comparer(func(a, b schema.IntegrationSchemaVersion) bool {
isAZero := reflect.ValueOf(a).IsZero()
isBZero := reflect.ValueOf(b).IsZero()
if isAZero && isBZero {
return true
}
if isAZero || isBZero {
return false
}
return a.Type() == b.Type() && a.Version == b.Version
})
var cur Integration
if integration != nil {
cur = *integration
}
cmp.Equal(cur, incoming, cmp.Reporter(&reporter), settingsCmp, secureCmp, schemaCmp)
return IntegrationDiffReport{DiffReport: reporter.Diffs}
}
// HasReceiversDifferentProtectedFields returns true if the receiver has any protected fields that are different from the incoming receiver.
func HasReceiversDifferentProtectedFields(existing, incoming *Receiver) map[string][]schema.IntegrationFieldPath {
existingIntegrations := make(map[string]*Integration, len(existing.Integrations))
for _, integration := range existing.Integrations {
existingIntegrations[integration.UID] = integration
}
var result = make(map[string][]schema.IntegrationFieldPath)
for _, in := range incoming.Integrations {
if in.UID == "" {
continue
}
ex, ok := existingIntegrations[in.UID]
if !ok {
continue
}
paths := HasIntegrationsDifferentProtectedFields(ex, in)
if len(paths) > 0 {
result[in.UID] = paths
}
}
return result
}
// HasIntegrationsDifferentProtectedFields returns list of paths to protected fields that are different between two integrations.
func HasIntegrationsDifferentProtectedFields(existing, incoming *Integration) []schema.IntegrationFieldPath {
diff := existing.Diff(*incoming)
// The incoming receiver always has both secret and non-secret fields in Settings.
// So, if it's specified and happens to be sensitive, we consider it changed
var result []schema.IntegrationFieldPath
settingsDiff := diff.GetSettingsPaths()
for _, path := range settingsDiff {
if IsProtectedField(incoming.Config.Type(), path) {
result = append(result, path)
}
}
return result
}
// IsProtectedField returns true if the field at the given path is existing protected one.
// This includes:
// 1. URL fields marked as secure in the schema (e.g., webhook URLs with credentials)
// 2. URL fields NOT marked as secure but could contain credentials (e.g., API endpoints)
func IsProtectedField(integrationType schema.IntegrationType, path schema.IntegrationFieldPath) bool {
str := strings.ToLower(string(integrationType))
pathStr := path.String()
switch str {
case "prometheus-alertmanager":
return pathStr == "url"
case "dingding":
return pathStr == "url" // marked as secure
case "discord":
return pathStr == "url" // marked as secure (webhook URL)
case "googlechat":
return pathStr == "url" // marked as secure
case "jira":
return pathStr == "api_url"
case "kafka":
return pathStr == "kafkaRestProxy"
case "line":
return false
case "mqtt":
return pathStr == "brokerUrl"
case "oncall":
return pathStr == "url"
case "opsgenie":
return pathStr == "apiUrl"
case "pagerduty":
return pathStr == "url"
case "sensugo":
return pathStr == "url"
case "slack":
return pathStr == "url" || pathStr == "endpointUrl"
case "teams":
return pathStr == "url"
case "victorops":
return pathStr == "url" // marked as secure
case "webex":
return pathStr == "api_url"
case "webhook":
return pathStr == "url" ||
pathStr == "http_config.oauth2.token_url" ||
pathStr == "http_config.oauth2.proxy_config.proxy_url"
case "wecom":
return pathStr == "url" || // marked as secure
pathStr == "endpointUrl"
default:
return false
}
}
@@ -0,0 +1,262 @@
package models
import (
"slices"
"testing"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/alerting/receivers/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIntegrationDiff(t *testing.T) {
s, _ := alertingNotify.GetSchemaVersionForIntegration("webhook", schema.V1)
a := Integration{
UID: "test-uid",
Name: "test-name",
Config: s,
DisableResolveMessage: false,
Settings: map[string]any{
"url": "http://localhost",
"name": 123,
"flag": true,
"child": map[string]any{
"sub-form-field": "test",
},
},
SecureSettings: map[string]string{
"password": "12345",
"token": "token-12345",
},
}
t.Run("no diff if equal", func(t *testing.T) {
result := a.Diff(a)
assert.Empty(t, result)
})
t.Run("should deep compare settings", func(t *testing.T) {
b := a
b.Settings = map[string]any{
"url": "http://localhost:123",
"flag": false,
"child": map[string]any{
"sub-form-field": "test123",
"sub-child": map[string]any{
"test": "test",
},
},
}
result := a.Diff(b)
assert.ElementsMatch(t,
[]string{"Settings[url]", "Settings[name]", "Settings[flag]", "Settings[child][sub-form-field]", "Settings[child][sub-child]"},
result.Paths())
})
t.Run("should shallow compare schemas", func(t *testing.T) {
b := a
b.Config, _ = alertingNotify.GetSchemaVersionForIntegration("slack", schema.V1)
result := a.Diff(b)
assert.ElementsMatch(t,
[]string{"Config"},
result.Paths())
})
t.Run("should compare with zero objects", func(t *testing.T) {
result := a.Diff(Integration{})
assert.ElementsMatch(t,
[]string{
"UID",
"Name",
"Config",
"Settings[child]",
"Settings[flag]",
"Settings[name]",
"Settings[url]",
"SecureSettings[password]",
"SecureSettings[token]",
},
result.Paths())
})
}
func TestIntegrationDiffReport_GetSettingsPaths(t *testing.T) {
a := Integration{
UID: "test-uid",
Name: "test-name",
Config: schema.IntegrationSchemaVersion{},
DisableResolveMessage: false,
Settings: map[string]any{
"url": "http://localhost",
"child": map[string]any{
"field": "test",
"sub-child": map[string]any{
"test": "test",
},
},
},
}
testCases := []struct {
name string
left map[string]any
right map[string]any
paths []string
}{
{
name: "empty",
left: map[string]any{},
right: map[string]any{},
},
{
name: "left is empty",
left: map[string]any{},
right: map[string]any{
"field": "test",
},
paths: []string{"field"},
},
{
name: "right is empty",
left: map[string]any{
"field": "test",
},
right: map[string]any{},
paths: []string{"field"},
},
{
name: "expands nested",
left: map[string]any{
"field": map[string]any{
"sub-field": map[string]any{
"test": "test",
},
},
},
right: map[string]any{
"another": map[string]any{
"sub-field": map[string]any{
"test": "test",
},
},
},
paths: []string{
"field.sub-field.test",
"another.sub-field.test",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := a
b.Settings = tc.right
a.Settings = tc.left
diff := a.Diff(b)
actual := diff.GetSettingsPaths()
actualStrings := make([]string, 0, len(actual))
for _, f := range actual {
actualStrings = append(actualStrings, f.String())
}
assert.ElementsMatch(t, tc.paths, actualStrings)
})
}
}
func TestHasDifferentProtectedFields(t *testing.T) {
m := IntegrationMuts
testCase := []struct {
name string
existing Integration
incoming Integration
expected map[string][]string
}{
{
name: "different UID do not match",
existing: IntegrationGen(m.WithUID("existing"), m.WithValidConfig("webhook"))(),
incoming: IntegrationGen(
m.WithValidConfig("webhook"),
m.AddSetting("url", "http://some-other-url"),
m.WithUID("incoming"),
)(),
expected: nil,
},
{
name: "find url protected",
existing: IntegrationGen(m.WithUID("1"), m.WithValidConfig("webhook"))(),
incoming: IntegrationGen(
m.WithValidConfig("webhook"),
m.AddSetting("url", "http://some-other-url"),
m.AddSetting("http_config", map[string]any{
"oauth2": map[string]any{
"proxy_config": map[string]any{
"proxy_url": "http://some-other-url-proxy",
},
"token_url": "http://some-other-url-token",
},
}),
m.WithUID("1"),
)(),
expected: map[string][]string{
"1": {
"http_config.oauth2.proxy_config.proxy_url",
"http_config.oauth2.token_url",
"url",
},
},
},
{
name: "secure and protected", // simulate the situation when protected secured field is in secure settings but the incoming one has it in settings
existing: IntegrationGen(
m.WithUID("1"),
m.WithValidConfig("discord"),
m.RemoveSetting("url"),
m.WithSecureSettings(map[string]string{
"url": "<SECURED>",
}))(),
incoming: IntegrationGen(
m.WithValidConfig("discord"),
m.AddSetting("url", "http://some-other-url"),
m.WithSecureSettings(nil),
m.WithUID("1"),
)(),
expected: map[string][]string{
"1": {
"url",
},
},
},
}
for _, tc := range testCase {
t.Run(tc.name, func(t *testing.T) {
existing := &Receiver{
Integrations: []*Integration{
&tc.existing,
},
}
incoming := &Receiver{
Integrations: []*Integration{
&tc.incoming,
},
}
actual := HasReceiversDifferentProtectedFields(existing, incoming)
if len(tc.expected) == 0 {
require.Empty(t, actual)
return
}
actualStrings := make(map[string][]string, len(actual))
for uid, paths := range actual {
for _, path := range paths {
actualStrings[uid] = append(actualStrings[uid], path.String())
}
slices.Sort(actualStrings[uid])
}
assert.EqualValues(t, tc.expected, actualStrings)
})
}
}
+12
View File
@@ -973,6 +973,12 @@ func (a AlertInstanceMutators) WithAnnotations(annotations InstanceAnnotations)
}
}
func (a AlertInstanceMutators) WithResultFingerprint(fp string) AlertInstanceMutator {
return func(i *AlertInstance) {
i.ResultFingerprint = fp
}
}
type Mutator[T any] func(*T)
// CopyNotificationSettings creates a deep copy of NotificationSettings.
@@ -1451,6 +1457,12 @@ func (n IntegrationMutators) AddSecureSetting(key, val string) Mutator[Integrati
}
}
func (n IntegrationMutators) RemoveSetting(key string) Mutator[Integration] {
return func(c *Integration) {
delete(c.Settings, key)
}
}
func randomMapKey[K comparable, V any](m map[K]V) (K, V) {
randIdx := rand.Intn(len(m))
i := 0
+14 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/alerting/models"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/alerting/notify/nfstatus"
alertingTemplates "github.com/grafana/alerting/templates"
"github.com/prometheus/alertmanager/config"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
@@ -58,6 +59,7 @@ type alertmanager struct {
decryptFn alertingNotify.GetDecryptedValueFn
crypto Crypto
features featuremgmt.FeatureToggles
dynamicLimits alertingNotify.DynamicLimits
}
// maintenanceOptions represent the options for components that need maintenance on a frequency within the Alertmanager.
@@ -148,6 +150,16 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
return nil, err
}
limits := alertingNotify.DynamicLimits{
Dispatcher: nilLimits{},
Templates: alertingTemplates.Limits{
MaxTemplateOutputSize: cfg.UnifiedAlerting.AlertmanagerMaxTemplateOutputSize,
},
}
if err := limits.Templates.Validate(); err != nil {
return nil, fmt.Errorf("invalid template limits: %w", err)
}
am := &alertmanager{
Base: gam,
ConfigMetrics: m.AlertmanagerConfigMetrics,
@@ -158,6 +170,7 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
decryptFn: decryptFn,
crypto: crypto,
features: featureToggles,
dynamicLimits: limits,
}
return am, nil
@@ -382,7 +395,7 @@ func (am *alertmanager) applyConfig(ctx context.Context, cfg *apimodels.Postable
TimeIntervals: amConfig.TimeIntervals,
Templates: templates,
Receivers: receivers,
DispatcherLimits: &nilLimits{},
Limits: am.dynamicLimits,
Raw: rawConfig,
Hash: configHash,
})
@@ -333,7 +333,7 @@ func (moa *MultiOrgAlertmanager) SaveAndApplyAlertmanagerConfiguration(ctx conte
config.ExtraConfigs = extraConfigs
}
if err := moa.Crypto.ProcessSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers); err != nil {
if err := moa.Crypto.ProcessSecureSettings(ctx, org, config.AlertmanagerConfig.Receivers, nil); err != nil {
return fmt.Errorf("failed to post process Alertmanager configuration: %w", err)
}
+36 -7
View File
@@ -29,16 +29,18 @@ const (
cryptoPrefix = "crypto_"
)
type AuthorizeProtectedFn func(uid string, paths []schema.IntegrationFieldPath) error
// Crypto allows decryption of Alertmanager Configuration and encryption of arbitrary payloads.
type Crypto interface {
LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error
LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver, fn AuthorizeProtectedFn) error
Encrypt(ctx context.Context, payload []byte, opt secrets.EncryptionOptions) ([]byte, error)
Decrypt(ctx context.Context, payload []byte) ([]byte, error)
EncryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error
DecryptExtraConfigs(ctx context.Context, config *definitions.PostableUserConfig) error
getDecryptedSecret(r *definitions.PostableGrafanaReceiver, key string) (string, error)
ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver) error
ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver, fn AuthorizeProtectedFn) error
}
// alertmanagerCrypto implements decryption of Alertmanager configuration and encryption of arbitrary payloads based on Grafana's encryptions.
@@ -57,7 +59,7 @@ func NewCrypto(secrets secrets.Service, configs configurationStore, log log.Logg
}
// ProcessSecureSettings encrypts new secure settings and loads existing secure settings from the database.
func (c *alertmanagerCrypto) ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver) error {
func (c *alertmanagerCrypto) ProcessSecureSettings(ctx context.Context, orgId int64, recvs []*definitions.PostableApiReceiver, authorizeProtected AuthorizeProtectedFn) error {
// First, we encrypt the new or updated secure settings. Then, we load the existing secure settings from the database
// and add back any that weren't updated.
// We perform these steps in this order to ensure the hash of the secure settings remains stable when no secure
@@ -68,7 +70,7 @@ func (c *alertmanagerCrypto) ProcessSecureSettings(ctx context.Context, orgId in
return fmt.Errorf("failed to encrypt receivers: %w", err)
}
if err := c.LoadSecureSettings(ctx, orgId, recvs); err != nil {
if err := c.LoadSecureSettings(ctx, orgId, recvs, authorizeProtected); err != nil {
return err
}
@@ -167,7 +169,7 @@ func encryptReceiverConfigs(c []*definitions.PostableApiReceiver, encrypt defini
}
// LoadSecureSettings adds the corresponding unencrypted secrets stored to the list of input receivers.
func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver) error {
func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64, receivers []*definitions.PostableApiReceiver, authorizeProtected AuthorizeProtectedFn) error {
// Get the last known working configuration.
amConfig, err := c.configs.GetLatestAlertmanagerConfiguration(ctx, orgId)
if err != nil {
@@ -176,10 +178,10 @@ func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64
return fmt.Errorf("failed to get latest configuration: %w", err)
}
}
var currentConfig *definitions.PostableUserConfig
currentReceiverMap := make(map[string]*definitions.PostableGrafanaReceiver)
if amConfig != nil {
currentConfig, err := Load([]byte(amConfig.AlertmanagerConfiguration))
currentConfig, err = Load([]byte(amConfig.AlertmanagerConfiguration))
// If the current config is un-loadable, treat it as if it never existed. Providing a new, valid config should be able to "fix" this state.
if err != nil {
c.log.Warn("Last known alertmanager configuration was invalid. Overwriting...")
@@ -209,6 +211,33 @@ func (c *alertmanagerCrypto) LoadSecureSettings(ctx context.Context, orgId int64
return UnknownReceiverError{UID: gr.UID}
}
if authorizeProtected != nil {
incoming, errIn := legacy_storage.PostableGrafanaReceiverToIntegration(gr)
existing, errEx := legacy_storage.PostableGrafanaReceiverToIntegration(cgmr)
var secure []schema.IntegrationFieldPath
authz := true
if errIn == nil && errEx == nil {
secure = models.HasIntegrationsDifferentProtectedFields(existing, incoming)
authz = len(secure) > 0
}
// if conversion failed, consider there are changes and authorize
if authz && currentConfig != nil {
var receiverName string
NAME:
for _, rcv := range currentConfig.AlertmanagerConfig.Receivers {
for _, intg := range rcv.GrafanaManagedReceivers {
if intg.UID == cgmr.UID {
receiverName = rcv.Name
break NAME
}
}
}
if err := authorizeProtected(receiverName, secure); err != nil {
return err
}
}
}
// Frontend sends only the secure settings that have to be updated
// Therefore we have to copy from the last configuration only those secure settings not included in the request
for key, encryptedValue := range cgmr.SecureSettings {
@@ -80,6 +80,9 @@ type receiverAccessControlService interface {
AuthorizeUpdate(context.Context, identity.Requester, *models.Receiver) error
AuthorizeDeleteByUID(context.Context, identity.Requester, string) error
HasUpdateProtected(context.Context, identity.Requester, *models.Receiver) (bool, error)
AuthorizeUpdateProtected(context.Context, identity.Requester, *models.Receiver) error
Access(ctx context.Context, user identity.Requester, receivers ...*models.Receiver) (map[string]models.ReceiverPermissionSet, error)
}
@@ -474,6 +477,18 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
return nil, err
}
// if user does not have permissions to update protected, check the diff and return error if there is a change in protected fields
canUpdateProtected, _ := rs.authz.HasUpdateProtected(ctx, user, r)
if !canUpdateProtected {
diff := models.HasReceiversDifferentProtectedFields(existing, r)
if len(diff) > 0 {
err = rs.authz.AuthorizeUpdateProtected(ctx, user, r)
if err != nil {
return nil, makeProtectedFieldsAuthzError(err, diff)
}
}
}
// We need to perform two important steps to process settings on an updated integration:
// 1. Encrypt new or updated secret fields as they will arrive in plain text.
// 2. For updates, callers do not re-send unchanged secure settings and instead mark them in SecureFields. We need
@@ -0,0 +1,30 @@
package notifier
import (
"errors"
"slices"
"github.com/grafana/alerting/receivers/schema"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
)
func makeProtectedFieldsAuthzError(err error, diff map[string][]schema.IntegrationFieldPath) error {
var authzErr errutil.Error
if !errors.As(err, &authzErr) {
return err
}
if authzErr.PublicPayload == nil {
authzErr.PublicPayload = map[string]interface{}{}
}
fields := make(map[string][]string, len(diff))
for field, paths := range diff {
fields[field] = make([]string, len(paths))
for i, path := range paths {
fields[field][i] = path.String()
}
slices.Sort(fields[field])
}
authzErr.PublicPayload["changed_protected_fields"] = fields
return authzErr
}
@@ -659,8 +659,9 @@ func TestReceiverService_Update(t *testing.T) {
writer := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
accesscontrol.ActionAlertingNotificationsWrite: nil,
accesscontrol.ActionAlertingNotificationsRead: nil,
accesscontrol.ActionAlertingReceiversUpdateProtected: {models.ScopeReceiversAll},
},
}}
decryptUser := &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
@@ -1310,7 +1311,7 @@ func TestReceiverServiceAC_Update(t *testing.T) {
},
}}
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("webhook"))
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
@@ -1322,8 +1323,8 @@ func TestReceiverServiceAC_Update(t *testing.T) {
name string
permissions map[string][]string
existing []models.Receiver
hasAccess []models.Receiver
incoming []models.Receiver
hasAccess []models.Receiver
}{
{
name: "not authorized without permissions",
@@ -1411,6 +1412,43 @@ func TestReceiverServiceAC_Update(t *testing.T) {
existing: allReceivers(),
hasAccess: []models.Receiver{recv1, recv3},
},
{
name: "protected fields modified without permission",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversUpdate: {models.ScopeReceiversAll},
accesscontrol.ActionAlertingReceiversRead: {models.ScopeReceiversAll},
},
existing: []models.Receiver{
recv1,
},
incoming: []models.Receiver{
func() models.Receiver {
f := recv1.Clone()
f.Integrations[0].Settings["url"] = "https://example.com/new"
return f
}(),
},
hasAccess: nil,
},
{
name: "protected fields modified with permission",
permissions: map[string][]string{
accesscontrol.ActionAlertingReceiversUpdate: {models.ScopeReceiversAll},
accesscontrol.ActionAlertingReceiversRead: {models.ScopeReceiversAll},
accesscontrol.ActionAlertingReceiversUpdateProtected: {models.ScopeReceiversAll},
},
existing: []models.Receiver{
recv1,
},
incoming: []models.Receiver{
func() models.Receiver {
f := recv1.Clone()
f.Integrations[0].Settings["url"] = "https://example.com/new"
return f
}(),
},
hasAccess: []models.Receiver{recv1},
},
}
for _, tc := range testCases {
@@ -1436,7 +1474,11 @@ func TestReceiverServiceAC_Update(t *testing.T) {
}
return false
}
for _, recv := range allReceivers() {
incoming := allReceivers()
if tc.incoming != nil {
incoming = tc.incoming
}
for _, recv := range incoming {
clone := recv.Clone()
clone.Version = versions[recv.UID]
response, err := sut.UpdateReceiver(context.Background(), &clone, nil, orgId, usr)
@@ -1734,6 +1776,7 @@ func TestReceiverService_AccessControlMetadata(t *testing.T) {
expectedPermissions.Set(models.ReceiverPermissionAdmin, false)
expectedPermissions.Set(models.ReceiverPermissionWrite, false)
expectedPermissions.Set(models.ReceiverPermissionDelete, false)
expectedPermissions.Set(models.ReceiverPermissionModifyProtected, false)
expectedPermissions.Set(models.ReceiverPermissionReadSecret, true)
expected := map[string]models.ReceiverPermissionSet{
@@ -44,6 +44,7 @@ type AnnotationBackend struct {
type RuleStore interface {
GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) (*ngmodels.AlertRule, error)
GetUserVisibleNamespaces(ctx context.Context, orgID int64, user identity.Requester) (map[string]*folder.Folder, error)
GetAlertRuleVersionFolders(ctx context.Context, orgID int64, guid string) ([]string, error)
}
type AnnotationStore interface {

Some files were not shown because too many files have changed in this diff Show More