Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
794b094044 | ||
|
|
91f9caa4ef | ||
|
|
02839523a0 | ||
|
|
24b6ce3bad | ||
|
|
b550c12e40 | ||
|
|
d8e3dda0e2 | ||
|
|
e4e45b9271 | ||
|
|
3f23ae36c9 | ||
|
|
5272cfcc99 | ||
|
|
151b81361c | ||
|
|
90a830ad71 | ||
|
|
3a9559c8d0 |
+1
-1
@@ -185,7 +185,6 @@
|
||||
/pkg/services/search/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/searchusers/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/secrets/ @grafana/grafana-operator-experience-squad
|
||||
/pkg/services/setting/ @grafana/grafana-backend-services-squad
|
||||
/pkg/services/shorturls/ @grafana/sharing-squad
|
||||
/pkg/services/sqlstore/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/ssosettings/ @grafana/identity-squad
|
||||
@@ -1323,6 +1322,7 @@ embed.go @grafana/grafana-as-code
|
||||
/conf/provisioning/datasources/ @grafana/plugins-platform-backend
|
||||
/conf/provisioning/plugins/ @grafana/plugins-platform-backend
|
||||
/conf/provisioning/sample/ @grafana/grafana-git-ui-sync-team
|
||||
/conf/apiextensions.ini @grafana/grafana-app-platform-squad
|
||||
|
||||
# Security
|
||||
/relyance.yaml @grafana/security-team
|
||||
|
||||
+4
-1
@@ -71,7 +71,6 @@ public/css/*.min.css
|
||||
.vs/
|
||||
.cursor/
|
||||
.devcontainer/
|
||||
.claude/
|
||||
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
@@ -135,6 +134,10 @@ profile.cov
|
||||
/pkg/operators/enterprise_*
|
||||
/pkg/operators/**/enterprise_*
|
||||
|
||||
# Enterprise apiextensions server
|
||||
pkg/registry/apis/apiextensions/*
|
||||
!pkg/registry/apis/apiextensions/register.go
|
||||
|
||||
debug.test
|
||||
/examples/*/dist
|
||||
/packaging/**/*.rpm
|
||||
|
||||
@@ -22,8 +22,6 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.4.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
|
||||
@@ -34,20 +32,15 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/smithy-go v1.23.1 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/bluele/gcache v0.0.2 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
|
||||
github.com/bwmarrin/snowflake v0.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
@@ -128,15 +121,11 @@ require (
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/nikunjy/rules v1.5.0 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/open-feature/go-sdk v1.16.0 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
@@ -153,7 +142,6 @@ require (
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect
|
||||
github.com/tjhop/slog-gokit v0.1.5 // indirect
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -171,8 +159,6 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.6.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
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -13,8 +9,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFezVH1C4=
|
||||
github.com/apache/arrow-go/v18 v18.4.1/go.mod h1:tLyFubsAl17bvFdUAy24bsSvA/6ww95Iqi67fTpGu3E=
|
||||
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
|
||||
@@ -37,18 +31,12 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
@@ -64,16 +52,12 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
|
||||
@@ -326,8 +310,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw=
|
||||
github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
@@ -342,12 +324,6 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
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/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
|
||||
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 h1:megzzlQGjsRVWDX8oJnLaa5eEcsAHekiL4Uvl3jSAcY=
|
||||
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6/go.mod h1:K1gDKvt76CGFLSUMHUydd5ba2V5Cv69gQZsdbnXhAm8=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 h1:WinefYxeVx5rV0uQmuWbxQf8iACu/JiRubo5w0saToc=
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6/go.mod h1:Dwcaoma6lZVqYwyfVlY7eB6RXbG+Ju3b9cnpTlUN+Hc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
@@ -421,10 +397,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM=
|
||||
github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8=
|
||||
github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc=
|
||||
github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM=
|
||||
github.com/tjhop/slog-gokit v0.1.5 h1:ayloIUi5EK2QYB8eY4DOPO95/mRtMW42lUkp3quJohc=
|
||||
github.com/tjhop/slog-gokit v0.1.5/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
@@ -474,12 +446,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
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=
|
||||
|
||||
@@ -1,29 +1,12 @@
|
||||
package plugins
|
||||
|
||||
pluginMetaV0Alpha1: {
|
||||
kind: "PluginMeta"
|
||||
kind: "PluginMeta"
|
||||
plural: "pluginsmeta"
|
||||
scope: "Namespaced"
|
||||
scope: "Namespaced"
|
||||
schema: {
|
||||
spec: {
|
||||
pluginJson: #JSONData
|
||||
module?: {
|
||||
path: string
|
||||
hash?: string
|
||||
loadingStrategy?: "fetch" | "script"
|
||||
}
|
||||
baseURL?: string
|
||||
signature?: {
|
||||
status: "internal" | "valid" | "invalid" | "modified" | "unsigned"
|
||||
type?: "grafana" | "commercial" | "community" | "private" | "private-glob"
|
||||
org?: string
|
||||
}
|
||||
angular?: {
|
||||
detected: bool
|
||||
}
|
||||
translations?: [string]: string
|
||||
// +listType=atomic
|
||||
children?: [...string]
|
||||
pluginJSON: #JSONData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-73
@@ -207,20 +207,13 @@ func NewPluginMetaExtensions() *PluginMetaExtensions {
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaSpec struct {
|
||||
PluginJson PluginMetaJSONData `json:"pluginJson"`
|
||||
Module *PluginMetaV0alpha1SpecModule `json:"module,omitempty"`
|
||||
BaseURL *string `json:"baseURL,omitempty"`
|
||||
Signature *PluginMetaV0alpha1SpecSignature `json:"signature,omitempty"`
|
||||
Angular *PluginMetaV0alpha1SpecAngular `json:"angular,omitempty"`
|
||||
Translations map[string]string `json:"translations,omitempty"`
|
||||
// +listType=atomic
|
||||
Children []string `json:"children,omitempty"`
|
||||
PluginJSON PluginMetaJSONData `json:"pluginJSON"`
|
||||
}
|
||||
|
||||
// NewPluginMetaSpec creates a new PluginMetaSpec object.
|
||||
func NewPluginMetaSpec() *PluginMetaSpec {
|
||||
return &PluginMetaSpec{
|
||||
PluginJson: *NewPluginMetaJSONData(),
|
||||
PluginJSON: *NewPluginMetaJSONData(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,40 +411,6 @@ func NewPluginMetaV0alpha1ExtensionsExtensionPoints() *PluginMetaV0alpha1Extensi
|
||||
return &PluginMetaV0alpha1ExtensionsExtensionPoints{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecModule struct {
|
||||
Path string `json:"path"`
|
||||
Hash *string `json:"hash,omitempty"`
|
||||
LoadingStrategy *PluginMetaV0alpha1SpecModuleLoadingStrategy `json:"loadingStrategy,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1SpecModule creates a new PluginMetaV0alpha1SpecModule object.
|
||||
func NewPluginMetaV0alpha1SpecModule() *PluginMetaV0alpha1SpecModule {
|
||||
return &PluginMetaV0alpha1SpecModule{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecSignature struct {
|
||||
Status PluginMetaV0alpha1SpecSignatureStatus `json:"status"`
|
||||
Type *PluginMetaV0alpha1SpecSignatureType `json:"type,omitempty"`
|
||||
Org *string `json:"org,omitempty"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1SpecSignature creates a new PluginMetaV0alpha1SpecSignature object.
|
||||
func NewPluginMetaV0alpha1SpecSignature() *PluginMetaV0alpha1SpecSignature {
|
||||
return &PluginMetaV0alpha1SpecSignature{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecAngular struct {
|
||||
Detected bool `json:"detected"`
|
||||
}
|
||||
|
||||
// NewPluginMetaV0alpha1SpecAngular creates a new PluginMetaV0alpha1SpecAngular object.
|
||||
func NewPluginMetaV0alpha1SpecAngular() *PluginMetaV0alpha1SpecAngular {
|
||||
return &PluginMetaV0alpha1SpecAngular{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaJSONDataType string
|
||||
|
||||
@@ -512,33 +471,3 @@ const (
|
||||
PluginMetaV0alpha1DependenciesPluginsTypeDatasource PluginMetaV0alpha1DependenciesPluginsType = "datasource"
|
||||
PluginMetaV0alpha1DependenciesPluginsTypePanel PluginMetaV0alpha1DependenciesPluginsType = "panel"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecModuleLoadingStrategy string
|
||||
|
||||
const (
|
||||
PluginMetaV0alpha1SpecModuleLoadingStrategyFetch PluginMetaV0alpha1SpecModuleLoadingStrategy = "fetch"
|
||||
PluginMetaV0alpha1SpecModuleLoadingStrategyScript PluginMetaV0alpha1SpecModuleLoadingStrategy = "script"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecSignatureStatus string
|
||||
|
||||
const (
|
||||
PluginMetaV0alpha1SpecSignatureStatusInternal PluginMetaV0alpha1SpecSignatureStatus = "internal"
|
||||
PluginMetaV0alpha1SpecSignatureStatusValid PluginMetaV0alpha1SpecSignatureStatus = "valid"
|
||||
PluginMetaV0alpha1SpecSignatureStatusInvalid PluginMetaV0alpha1SpecSignatureStatus = "invalid"
|
||||
PluginMetaV0alpha1SpecSignatureStatusModified PluginMetaV0alpha1SpecSignatureStatus = "modified"
|
||||
PluginMetaV0alpha1SpecSignatureStatusUnsigned PluginMetaV0alpha1SpecSignatureStatus = "unsigned"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type PluginMetaV0alpha1SpecSignatureType string
|
||||
|
||||
const (
|
||||
PluginMetaV0alpha1SpecSignatureTypeGrafana PluginMetaV0alpha1SpecSignatureType = "grafana"
|
||||
PluginMetaV0alpha1SpecSignatureTypeCommercial PluginMetaV0alpha1SpecSignatureType = "commercial"
|
||||
PluginMetaV0alpha1SpecSignatureTypeCommunity PluginMetaV0alpha1SpecSignatureType = "community"
|
||||
PluginMetaV0alpha1SpecSignatureTypePrivate PluginMetaV0alpha1SpecSignatureType = "private"
|
||||
PluginMetaV0alpha1SpecSignatureTypePrivateGlob PluginMetaV0alpha1SpecSignatureType = "private-glob"
|
||||
)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -87,47 +87,8 @@ func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
spec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: gcomMeta.JSON,
|
||||
}
|
||||
|
||||
if gcomMeta.VersionSignatureType != "" {
|
||||
signature := &pluginsv0alpha1.PluginMetaV0alpha1SpecSignature{
|
||||
Status: pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusValid,
|
||||
}
|
||||
|
||||
switch gcomMeta.VersionSignatureType {
|
||||
case "grafana":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeGrafana
|
||||
signature.Type = &sigType
|
||||
case "commercial":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommercial
|
||||
signature.Type = &sigType
|
||||
case "community":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommunity
|
||||
signature.Type = &sigType
|
||||
case "private":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivate
|
||||
signature.Type = &sigType
|
||||
case "private-glob":
|
||||
sigType := pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivateGlob
|
||||
signature.Type = &sigType
|
||||
}
|
||||
|
||||
if gcomMeta.VersionSignedByOrg != "" {
|
||||
signature.Org = &gcomMeta.VersionSignedByOrg
|
||||
}
|
||||
|
||||
spec.Signature = signature
|
||||
}
|
||||
|
||||
// Set angular info
|
||||
spec.Angular = &pluginsv0alpha1.PluginMetaV0alpha1SpecAngular{
|
||||
Detected: gcomMeta.AngularDetected,
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Meta: spec,
|
||||
Meta: gcomMeta.JSON,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
@@ -135,25 +96,25 @@ func (p *CloudProvider) GetMeta(ctx context.Context, pluginID, version string) (
|
||||
// grafanaComPluginVersionMeta represents the response from grafana.com API
|
||||
// GET /api/plugins/{pluginId}/versions/{version}
|
||||
type grafanaComPluginVersionMeta struct {
|
||||
PluginID string `json:"pluginSlug"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
Commit string `json:"commit"`
|
||||
Description string `json:"description"`
|
||||
Keywords []string `json:"keywords"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"`
|
||||
Readme string `json:"readme"`
|
||||
Downloads int `json:"downloads"`
|
||||
Verified bool `json:"verified"`
|
||||
Status string `json:"status"`
|
||||
StatusContext string `json:"statusContext"`
|
||||
DownloadSlug string `json:"downloadSlug"`
|
||||
VersionSignatureType string `json:"versionSignatureType"`
|
||||
VersionSignedByOrg string `json:"versionSignedByOrg"`
|
||||
VersionSignedByOrgName string `json:"versionSignedByOrgName"`
|
||||
Packages struct {
|
||||
PluginID string `json:"pluginSlug"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
Commit string `json:"commit"`
|
||||
Description string `json:"description"`
|
||||
Keywords []string `json:"keywords"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
JSON pluginsv0alpha1.PluginMetaJSONData `json:"json"`
|
||||
Readme string `json:"readme"`
|
||||
Downloads int `json:"downloads"`
|
||||
Verified bool `json:"verified"`
|
||||
Status string `json:"status"`
|
||||
StatusContext string `json:"statusContext"`
|
||||
DownloadSlug string `json:"downloadSlug"`
|
||||
SignatureType string `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
Packages struct {
|
||||
Any struct {
|
||||
Md5 string `json:"md5"`
|
||||
Sha256 string `json:"sha256"`
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestCloudProvider_GetMeta(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedMeta, result.Meta.PluginJson)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, defaultCloudTTL, result.TTL)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,585 +0,0 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData.
|
||||
// nolint:gocyclo
|
||||
func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData {
|
||||
meta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: jsonData.ID,
|
||||
Name: jsonData.Name,
|
||||
}
|
||||
|
||||
// Map plugin type
|
||||
switch jsonData.Type {
|
||||
case plugins.TypeApp:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp
|
||||
case plugins.TypeDataSource:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource
|
||||
case plugins.TypePanel:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel
|
||||
case plugins.TypeRenderer:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer
|
||||
}
|
||||
|
||||
// Map Info
|
||||
meta.Info = pluginsv0alpha1.PluginMetaInfo{
|
||||
Keywords: jsonData.Info.Keywords,
|
||||
Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{
|
||||
Small: jsonData.Info.Logos.Small,
|
||||
Large: jsonData.Info.Logos.Large,
|
||||
},
|
||||
Updated: jsonData.Info.Updated,
|
||||
Version: jsonData.Info.Version,
|
||||
}
|
||||
|
||||
if jsonData.Info.Description != "" {
|
||||
meta.Info.Description = &jsonData.Info.Description
|
||||
}
|
||||
|
||||
if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" {
|
||||
author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{}
|
||||
if jsonData.Info.Author.Name != "" {
|
||||
author.Name = &jsonData.Info.Author.Name
|
||||
}
|
||||
if jsonData.Info.Author.URL != "" {
|
||||
author.Url = &jsonData.Info.Author.URL
|
||||
}
|
||||
meta.Info.Author = author
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Links) > 0 {
|
||||
meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links))
|
||||
for _, link := range jsonData.Info.Links {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{}
|
||||
if link.Name != "" {
|
||||
v0Link.Name = &link.Name
|
||||
}
|
||||
if link.URL != "" {
|
||||
v0Link.Url = &link.URL
|
||||
}
|
||||
meta.Info.Links = append(meta.Info.Links, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Screenshots) > 0 {
|
||||
meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots))
|
||||
for _, screenshot := range jsonData.Info.Screenshots {
|
||||
v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{}
|
||||
if screenshot.Name != "" {
|
||||
v0Screenshot.Name = &screenshot.Name
|
||||
}
|
||||
if screenshot.Path != "" {
|
||||
v0Screenshot.Path = &screenshot.Path
|
||||
}
|
||||
meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Dependencies
|
||||
meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{
|
||||
GrafanaDependency: jsonData.Dependencies.GrafanaDependency,
|
||||
}
|
||||
|
||||
if jsonData.Dependencies.GrafanaVersion != "" {
|
||||
meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Plugins) > 0 {
|
||||
meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins))
|
||||
for _, dep := range jsonData.Dependencies.Plugins {
|
||||
var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType
|
||||
switch dep.Type {
|
||||
case "app":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp
|
||||
case "datasource":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource
|
||||
case "panel":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel
|
||||
}
|
||||
meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{
|
||||
Id: dep.ID,
|
||||
Type: depType,
|
||||
Name: dep.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 {
|
||||
meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{
|
||||
ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents,
|
||||
}
|
||||
}
|
||||
|
||||
// Map optional boolean fields
|
||||
if jsonData.Alerting {
|
||||
meta.Alerting = &jsonData.Alerting
|
||||
}
|
||||
if jsonData.Annotations {
|
||||
meta.Annotations = &jsonData.Annotations
|
||||
}
|
||||
if jsonData.AutoEnabled {
|
||||
meta.AutoEnabled = &jsonData.AutoEnabled
|
||||
}
|
||||
if jsonData.Backend {
|
||||
meta.Backend = &jsonData.Backend
|
||||
}
|
||||
if jsonData.BuiltIn {
|
||||
meta.BuiltIn = &jsonData.BuiltIn
|
||||
}
|
||||
if jsonData.HideFromList {
|
||||
meta.HideFromList = &jsonData.HideFromList
|
||||
}
|
||||
if jsonData.Logs {
|
||||
meta.Logs = &jsonData.Logs
|
||||
}
|
||||
if jsonData.Metrics {
|
||||
meta.Metrics = &jsonData.Metrics
|
||||
}
|
||||
if jsonData.MultiValueFilterOperators {
|
||||
meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators
|
||||
}
|
||||
if jsonData.Preload {
|
||||
meta.Preload = &jsonData.Preload
|
||||
}
|
||||
if jsonData.SkipDataQuery {
|
||||
meta.SkipDataQuery = &jsonData.SkipDataQuery
|
||||
}
|
||||
if jsonData.Streaming {
|
||||
meta.Streaming = &jsonData.Streaming
|
||||
}
|
||||
if jsonData.Tracing {
|
||||
meta.Tracing = &jsonData.Tracing
|
||||
}
|
||||
|
||||
// Map category
|
||||
if jsonData.Category != "" {
|
||||
var category pluginsv0alpha1.PluginMetaJSONDataCategory
|
||||
switch jsonData.Category {
|
||||
case "tsdb":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb
|
||||
case "logging":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging
|
||||
case "cloud":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud
|
||||
case "tracing":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing
|
||||
case "profiling":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling
|
||||
case "sql":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategorySql
|
||||
case "enterprise":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise
|
||||
case "iot":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot
|
||||
case "other":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
default:
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
}
|
||||
meta.Category = &category
|
||||
}
|
||||
|
||||
// Map state
|
||||
if jsonData.State != "" {
|
||||
var state pluginsv0alpha1.PluginMetaJSONDataState
|
||||
switch jsonData.State {
|
||||
case plugins.ReleaseStateAlpha:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha
|
||||
case plugins.ReleaseStateBeta:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateBeta
|
||||
default:
|
||||
}
|
||||
if state != "" {
|
||||
meta.State = &state
|
||||
}
|
||||
}
|
||||
|
||||
// Map executable
|
||||
if jsonData.Executable != "" {
|
||||
meta.Executable = &jsonData.Executable
|
||||
}
|
||||
|
||||
// Map QueryOptions
|
||||
if len(jsonData.QueryOptions) > 0 {
|
||||
queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{}
|
||||
if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok {
|
||||
queryOptions.MaxDataPoints = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["minInterval"]; ok {
|
||||
queryOptions.MinInterval = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok {
|
||||
queryOptions.CacheTimeout = &val
|
||||
}
|
||||
meta.QueryOptions = queryOptions
|
||||
}
|
||||
|
||||
// Map Includes
|
||||
if len(jsonData.Includes) > 0 {
|
||||
meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes))
|
||||
for _, include := range jsonData.Includes {
|
||||
v0Include := pluginsv0alpha1.PluginMetaInclude{}
|
||||
if include.UID != "" {
|
||||
v0Include.Uid = &include.UID
|
||||
}
|
||||
if include.Type != "" {
|
||||
var includeType pluginsv0alpha1.PluginMetaIncludeType
|
||||
switch include.Type {
|
||||
case "dashboard":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard
|
||||
case "page":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePage
|
||||
case "panel":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel
|
||||
case "datasource":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource
|
||||
}
|
||||
v0Include.Type = &includeType
|
||||
}
|
||||
if include.Name != "" {
|
||||
v0Include.Name = &include.Name
|
||||
}
|
||||
if include.Component != "" {
|
||||
v0Include.Component = &include.Component
|
||||
}
|
||||
if include.Role != "" {
|
||||
var role pluginsv0alpha1.PluginMetaIncludeRole
|
||||
switch include.Role {
|
||||
case "Admin":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin
|
||||
case "Editor":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleEditor
|
||||
case "Viewer":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleViewer
|
||||
}
|
||||
v0Include.Role = &role
|
||||
}
|
||||
if include.Action != "" {
|
||||
v0Include.Action = &include.Action
|
||||
}
|
||||
if include.Path != "" {
|
||||
v0Include.Path = &include.Path
|
||||
}
|
||||
if include.AddToNav {
|
||||
v0Include.AddToNav = &include.AddToNav
|
||||
}
|
||||
if include.DefaultNav {
|
||||
v0Include.DefaultNav = &include.DefaultNav
|
||||
}
|
||||
if include.Icon != "" {
|
||||
v0Include.Icon = &include.Icon
|
||||
}
|
||||
meta.Includes = append(meta.Includes, v0Include)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Routes
|
||||
if len(jsonData.Routes) > 0 {
|
||||
meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes))
|
||||
for _, route := range jsonData.Routes {
|
||||
v0Route := pluginsv0alpha1.PluginMetaRoute{}
|
||||
if route.Path != "" {
|
||||
v0Route.Path = &route.Path
|
||||
}
|
||||
if route.Method != "" {
|
||||
v0Route.Method = &route.Method
|
||||
}
|
||||
if route.URL != "" {
|
||||
v0Route.Url = &route.URL
|
||||
}
|
||||
if route.ReqRole != "" {
|
||||
reqRole := string(route.ReqRole)
|
||||
v0Route.ReqRole = &reqRole
|
||||
}
|
||||
if route.ReqAction != "" {
|
||||
v0Route.ReqAction = &route.ReqAction
|
||||
}
|
||||
if len(route.Headers) > 0 {
|
||||
headers := make([]string, 0, len(route.Headers))
|
||||
for _, header := range route.Headers {
|
||||
headers = append(headers, header.Name+": "+header.Content)
|
||||
}
|
||||
v0Route.Headers = headers
|
||||
}
|
||||
if len(route.URLParams) > 0 {
|
||||
v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams))
|
||||
for _, param := range route.URLParams {
|
||||
v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{}
|
||||
if param.Name != "" {
|
||||
v0Param.Name = ¶m.Name
|
||||
}
|
||||
if param.Content != "" {
|
||||
v0Param.Content = ¶m.Content
|
||||
}
|
||||
v0Route.UrlParams = append(v0Route.UrlParams, v0Param)
|
||||
}
|
||||
}
|
||||
if route.TokenAuth != nil {
|
||||
v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{}
|
||||
if route.TokenAuth.Url != "" {
|
||||
v0Route.TokenAuth.Url = &route.TokenAuth.Url
|
||||
}
|
||||
if len(route.TokenAuth.Scopes) > 0 {
|
||||
v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes
|
||||
}
|
||||
if len(route.TokenAuth.Params) > 0 {
|
||||
v0Route.TokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.TokenAuth.Params {
|
||||
v0Route.TokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if route.JwtTokenAuth != nil {
|
||||
v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{}
|
||||
if route.JwtTokenAuth.Url != "" {
|
||||
v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url
|
||||
}
|
||||
if len(route.JwtTokenAuth.Scopes) > 0 {
|
||||
v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes
|
||||
}
|
||||
if len(route.JwtTokenAuth.Params) > 0 {
|
||||
v0Route.JwtTokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.JwtTokenAuth.Params {
|
||||
v0Route.JwtTokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(route.Body) > 0 {
|
||||
var bodyMap map[string]interface{}
|
||||
if err := json.Unmarshal(route.Body, &bodyMap); err == nil {
|
||||
v0Route.Body = bodyMap
|
||||
}
|
||||
}
|
||||
meta.Routes = append(meta.Routes, v0Route)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Extensions
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
|
||||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions := &pluginsv0alpha1.PluginMetaExtensions{}
|
||||
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 {
|
||||
extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks))
|
||||
for _, link := range jsonData.Extensions.AddedLinks {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{
|
||||
Targets: link.Targets,
|
||||
Title: link.Title,
|
||||
}
|
||||
if link.Description != "" {
|
||||
v0Link.Description = &link.Description
|
||||
}
|
||||
extensions.AddedLinks = append(extensions.AddedLinks, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.AddedComponents) > 0 {
|
||||
extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents))
|
||||
for _, comp := range jsonData.Extensions.AddedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{
|
||||
Targets: comp.Targets,
|
||||
Title: comp.Title,
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.AddedComponents = append(extensions.AddedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExposedComponents) > 0 {
|
||||
extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
|
||||
for _, comp := range jsonData.Extensions.ExposedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{
|
||||
Id: comp.Id,
|
||||
}
|
||||
if comp.Title != "" {
|
||||
v0Comp.Title = &comp.Title
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints))
|
||||
for _, point := range jsonData.Extensions.ExtensionPoints {
|
||||
v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{
|
||||
Id: point.Id,
|
||||
}
|
||||
if point.Title != "" {
|
||||
v0Point.Title = &point.Title
|
||||
}
|
||||
if point.Description != "" {
|
||||
v0Point.Description = &point.Description
|
||||
}
|
||||
extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point)
|
||||
}
|
||||
}
|
||||
|
||||
meta.Extensions = extensions
|
||||
}
|
||||
|
||||
// Map Roles
|
||||
if len(jsonData.Roles) > 0 {
|
||||
meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles))
|
||||
for _, role := range jsonData.Roles {
|
||||
v0Role := pluginsv0alpha1.PluginMetaRole{
|
||||
Grants: role.Grants,
|
||||
}
|
||||
if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{}
|
||||
if role.Role.Name != "" {
|
||||
v0RoleRole.Name = &role.Role.Name
|
||||
}
|
||||
if role.Role.Description != "" {
|
||||
v0RoleRole.Description = &role.Role.Description
|
||||
}
|
||||
if len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions))
|
||||
for _, perm := range role.Role.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm)
|
||||
}
|
||||
}
|
||||
v0Role.Role = v0RoleRole
|
||||
}
|
||||
meta.Roles = append(meta.Roles, v0Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Map IAM
|
||||
if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 {
|
||||
iam := &pluginsv0alpha1.PluginMetaIAM{
|
||||
Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)),
|
||||
}
|
||||
for _, perm := range jsonData.IAM.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
iam.Permissions = append(iam.Permissions, v0Perm)
|
||||
}
|
||||
meta.Iam = iam
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
// pluginStorePluginToPluginMetaSpec converts a pluginstore.Plugin to a pluginsv0alpha1.PluginMetaSpec.
|
||||
// This is similar to pluginToPluginMetaSpec but works with the plugin store DTO.
|
||||
// loadingStrategy and moduleHash are optional calculated values that can be provided.
|
||||
func pluginStorePluginToPluginMetaSpec(plugin pluginstore.Plugin, loadingStrategy plugins.LoadingStrategy, moduleHash string) pluginsv0alpha1.PluginMetaSpec {
|
||||
spec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: jsonDataToPluginMetaJSONData(plugin.JSONData),
|
||||
}
|
||||
|
||||
if plugin.Module != "" {
|
||||
module := &pluginsv0alpha1.PluginMetaV0alpha1SpecModule{
|
||||
Path: plugin.Module,
|
||||
}
|
||||
if moduleHash != "" {
|
||||
module.Hash = &moduleHash
|
||||
}
|
||||
if loadingStrategy != "" {
|
||||
var ls pluginsv0alpha1.PluginMetaV0alpha1SpecModuleLoadingStrategy
|
||||
switch loadingStrategy {
|
||||
case plugins.LoadingStrategyFetch:
|
||||
ls = pluginsv0alpha1.PluginMetaV0alpha1SpecModuleLoadingStrategyFetch
|
||||
case plugins.LoadingStrategyScript:
|
||||
ls = pluginsv0alpha1.PluginMetaV0alpha1SpecModuleLoadingStrategyScript
|
||||
}
|
||||
module.LoadingStrategy = &ls
|
||||
}
|
||||
spec.Module = module
|
||||
}
|
||||
|
||||
if plugin.BaseURL != "" {
|
||||
spec.BaseURL = &plugin.BaseURL
|
||||
}
|
||||
|
||||
if plugin.Signature != "" {
|
||||
signature := &pluginsv0alpha1.PluginMetaV0alpha1SpecSignature{
|
||||
Status: convertSignatureStatus(plugin.Signature),
|
||||
}
|
||||
|
||||
if plugin.SignatureType != "" {
|
||||
sigType := convertSignatureType(plugin.SignatureType)
|
||||
signature.Type = &sigType
|
||||
}
|
||||
|
||||
if plugin.SignatureOrg != "" {
|
||||
signature.Org = &plugin.SignatureOrg
|
||||
}
|
||||
|
||||
spec.Signature = signature
|
||||
}
|
||||
|
||||
if len(plugin.Children) > 0 {
|
||||
spec.Children = plugin.Children
|
||||
}
|
||||
|
||||
spec.Angular = &pluginsv0alpha1.PluginMetaV0alpha1SpecAngular{
|
||||
Detected: plugin.Angular.Detected,
|
||||
}
|
||||
|
||||
if len(plugin.Translations) > 0 {
|
||||
spec.Translations = plugin.Translations
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
// convertSignatureStatus converts plugins.SignatureStatus to pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatus.
|
||||
func convertSignatureStatus(status plugins.SignatureStatus) pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatus {
|
||||
switch status {
|
||||
case plugins.SignatureStatusInternal:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusInternal
|
||||
case plugins.SignatureStatusValid:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusValid
|
||||
case plugins.SignatureStatusInvalid:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusInvalid
|
||||
case plugins.SignatureStatusModified:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusModified
|
||||
case plugins.SignatureStatusUnsigned:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusUnsigned
|
||||
default:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureStatusUnsigned
|
||||
}
|
||||
}
|
||||
|
||||
// convertSignatureType converts plugins.SignatureType to pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureType.
|
||||
func convertSignatureType(sigType plugins.SignatureType) pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureType {
|
||||
switch sigType {
|
||||
case plugins.SignatureTypeGrafana:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeGrafana
|
||||
case plugins.SignatureTypeCommercial:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommercial
|
||||
case plugins.SignatureTypeCommunity:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeCommunity
|
||||
case plugins.SignatureTypePrivate:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivate
|
||||
case plugins.SignatureTypePrivateGlob:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypePrivateGlob
|
||||
default:
|
||||
return pluginsv0alpha1.PluginMetaV0alpha1SpecSignatureTypeGrafana
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -22,7 +23,7 @@ const (
|
||||
// CoreProvider retrieves plugin metadata for core plugins.
|
||||
type CoreProvider struct {
|
||||
mu sync.RWMutex
|
||||
loadedPlugins map[string]pluginsv0alpha1.PluginMetaSpec
|
||||
loadedPlugins map[string]pluginsv0alpha1.PluginMetaJSONData
|
||||
initialized bool
|
||||
ttl time.Duration
|
||||
}
|
||||
@@ -35,7 +36,7 @@ func NewCoreProvider() *CoreProvider {
|
||||
// NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL.
|
||||
func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider {
|
||||
return &CoreProvider{
|
||||
loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaSpec),
|
||||
loadedPlugins: make(map[string]pluginsv0alpha1.PluginMetaJSONData),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
@@ -75,9 +76,9 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result
|
||||
p.initialized = true
|
||||
}
|
||||
|
||||
if spec, found := p.loadedPlugins[pluginID]; found {
|
||||
if meta, found := p.loadedPlugins[pluginID]; found {
|
||||
return &Result{
|
||||
Meta: spec,
|
||||
Meta: meta,
|
||||
TTL: p.ttl,
|
||||
}, nil
|
||||
}
|
||||
@@ -118,11 +119,485 @@ func (p *CoreProvider) loadPlugins(ctx context.Context) error {
|
||||
}
|
||||
|
||||
for _, bundle := range ps {
|
||||
spec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: jsonDataToPluginMetaJSONData(bundle.Primary.JSONData),
|
||||
}
|
||||
p.loadedPlugins[bundle.Primary.JSONData.ID] = spec
|
||||
meta := jsonDataToPluginMetaJSONData(bundle.Primary.JSONData)
|
||||
p.loadedPlugins[bundle.Primary.JSONData.ID] = meta
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// jsonDataToPluginMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.PluginMetaJSONData.
|
||||
// nolint:gocyclo
|
||||
func jsonDataToPluginMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.PluginMetaJSONData {
|
||||
meta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: jsonData.ID,
|
||||
Name: jsonData.Name,
|
||||
}
|
||||
|
||||
// Map plugin type
|
||||
switch jsonData.Type {
|
||||
case plugins.TypeApp:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeApp
|
||||
case plugins.TypeDataSource:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeDatasource
|
||||
case plugins.TypePanel:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypePanel
|
||||
case plugins.TypeRenderer:
|
||||
meta.Type = pluginsv0alpha1.PluginMetaJSONDataTypeRenderer
|
||||
}
|
||||
|
||||
// Map Info
|
||||
meta.Info = pluginsv0alpha1.PluginMetaInfo{
|
||||
Keywords: jsonData.Info.Keywords,
|
||||
Logos: pluginsv0alpha1.PluginMetaV0alpha1InfoLogos{
|
||||
Small: jsonData.Info.Logos.Small,
|
||||
Large: jsonData.Info.Logos.Large,
|
||||
},
|
||||
Updated: jsonData.Info.Updated,
|
||||
Version: jsonData.Info.Version,
|
||||
}
|
||||
|
||||
if jsonData.Info.Description != "" {
|
||||
meta.Info.Description = &jsonData.Info.Description
|
||||
}
|
||||
|
||||
if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" {
|
||||
author := &pluginsv0alpha1.PluginMetaV0alpha1InfoAuthor{}
|
||||
if jsonData.Info.Author.Name != "" {
|
||||
author.Name = &jsonData.Info.Author.Name
|
||||
}
|
||||
if jsonData.Info.Author.URL != "" {
|
||||
author.Url = &jsonData.Info.Author.URL
|
||||
}
|
||||
meta.Info.Author = author
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Links) > 0 {
|
||||
meta.Info.Links = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links))
|
||||
for _, link := range jsonData.Info.Links {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1InfoLinks{}
|
||||
if link.Name != "" {
|
||||
v0Link.Name = &link.Name
|
||||
}
|
||||
if link.URL != "" {
|
||||
v0Link.Url = &link.URL
|
||||
}
|
||||
meta.Info.Links = append(meta.Info.Links, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Info.Screenshots) > 0 {
|
||||
meta.Info.Screenshots = make([]pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots))
|
||||
for _, screenshot := range jsonData.Info.Screenshots {
|
||||
v0Screenshot := pluginsv0alpha1.PluginMetaV0alpha1InfoScreenshots{}
|
||||
if screenshot.Name != "" {
|
||||
v0Screenshot.Name = &screenshot.Name
|
||||
}
|
||||
if screenshot.Path != "" {
|
||||
v0Screenshot.Path = &screenshot.Path
|
||||
}
|
||||
meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Dependencies
|
||||
meta.Dependencies = pluginsv0alpha1.PluginMetaDependencies{
|
||||
GrafanaDependency: jsonData.Dependencies.GrafanaDependency,
|
||||
}
|
||||
|
||||
if jsonData.Dependencies.GrafanaVersion != "" {
|
||||
meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Plugins) > 0 {
|
||||
meta.Dependencies.Plugins = make([]pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins))
|
||||
for _, dep := range jsonData.Dependencies.Plugins {
|
||||
var depType pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsType
|
||||
switch dep.Type {
|
||||
case "app":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeApp
|
||||
case "datasource":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypeDatasource
|
||||
case "panel":
|
||||
depType = pluginsv0alpha1.PluginMetaV0alpha1DependenciesPluginsTypePanel
|
||||
}
|
||||
meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.PluginMetaV0alpha1DependenciesPlugins{
|
||||
Id: dep.ID,
|
||||
Type: depType,
|
||||
Name: dep.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 {
|
||||
meta.Dependencies.Extensions = &pluginsv0alpha1.PluginMetaV0alpha1DependenciesExtensions{
|
||||
ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents,
|
||||
}
|
||||
}
|
||||
|
||||
// Map optional boolean fields
|
||||
if jsonData.Alerting {
|
||||
meta.Alerting = &jsonData.Alerting
|
||||
}
|
||||
if jsonData.Annotations {
|
||||
meta.Annotations = &jsonData.Annotations
|
||||
}
|
||||
if jsonData.AutoEnabled {
|
||||
meta.AutoEnabled = &jsonData.AutoEnabled
|
||||
}
|
||||
if jsonData.Backend {
|
||||
meta.Backend = &jsonData.Backend
|
||||
}
|
||||
if jsonData.BuiltIn {
|
||||
meta.BuiltIn = &jsonData.BuiltIn
|
||||
}
|
||||
if jsonData.HideFromList {
|
||||
meta.HideFromList = &jsonData.HideFromList
|
||||
}
|
||||
if jsonData.Logs {
|
||||
meta.Logs = &jsonData.Logs
|
||||
}
|
||||
if jsonData.Metrics {
|
||||
meta.Metrics = &jsonData.Metrics
|
||||
}
|
||||
if jsonData.MultiValueFilterOperators {
|
||||
meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators
|
||||
}
|
||||
if jsonData.Preload {
|
||||
meta.Preload = &jsonData.Preload
|
||||
}
|
||||
if jsonData.SkipDataQuery {
|
||||
meta.SkipDataQuery = &jsonData.SkipDataQuery
|
||||
}
|
||||
if jsonData.Streaming {
|
||||
meta.Streaming = &jsonData.Streaming
|
||||
}
|
||||
if jsonData.Tracing {
|
||||
meta.Tracing = &jsonData.Tracing
|
||||
}
|
||||
|
||||
// Map category
|
||||
if jsonData.Category != "" {
|
||||
var category pluginsv0alpha1.PluginMetaJSONDataCategory
|
||||
switch jsonData.Category {
|
||||
case "tsdb":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTsdb
|
||||
case "logging":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryLogging
|
||||
case "cloud":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryCloud
|
||||
case "tracing":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryTracing
|
||||
case "profiling":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryProfiling
|
||||
case "sql":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategorySql
|
||||
case "enterprise":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryEnterprise
|
||||
case "iot":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryIot
|
||||
case "other":
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
default:
|
||||
category = pluginsv0alpha1.PluginMetaJSONDataCategoryOther
|
||||
}
|
||||
meta.Category = &category
|
||||
}
|
||||
|
||||
// Map state
|
||||
if jsonData.State != "" {
|
||||
var state pluginsv0alpha1.PluginMetaJSONDataState
|
||||
switch jsonData.State {
|
||||
case plugins.ReleaseStateAlpha:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateAlpha
|
||||
case plugins.ReleaseStateBeta:
|
||||
state = pluginsv0alpha1.PluginMetaJSONDataStateBeta
|
||||
default:
|
||||
}
|
||||
if state != "" {
|
||||
meta.State = &state
|
||||
}
|
||||
}
|
||||
|
||||
// Map executable
|
||||
if jsonData.Executable != "" {
|
||||
meta.Executable = &jsonData.Executable
|
||||
}
|
||||
|
||||
// Map QueryOptions
|
||||
if len(jsonData.QueryOptions) > 0 {
|
||||
queryOptions := &pluginsv0alpha1.PluginMetaQueryOptions{}
|
||||
if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok {
|
||||
queryOptions.MaxDataPoints = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["minInterval"]; ok {
|
||||
queryOptions.MinInterval = &val
|
||||
}
|
||||
if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok {
|
||||
queryOptions.CacheTimeout = &val
|
||||
}
|
||||
meta.QueryOptions = queryOptions
|
||||
}
|
||||
|
||||
// Map Includes
|
||||
if len(jsonData.Includes) > 0 {
|
||||
meta.Includes = make([]pluginsv0alpha1.PluginMetaInclude, 0, len(jsonData.Includes))
|
||||
for _, include := range jsonData.Includes {
|
||||
v0Include := pluginsv0alpha1.PluginMetaInclude{}
|
||||
if include.UID != "" {
|
||||
v0Include.Uid = &include.UID
|
||||
}
|
||||
if include.Type != "" {
|
||||
var includeType pluginsv0alpha1.PluginMetaIncludeType
|
||||
switch include.Type {
|
||||
case "dashboard":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDashboard
|
||||
case "page":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePage
|
||||
case "panel":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypePanel
|
||||
case "datasource":
|
||||
includeType = pluginsv0alpha1.PluginMetaIncludeTypeDatasource
|
||||
}
|
||||
v0Include.Type = &includeType
|
||||
}
|
||||
if include.Name != "" {
|
||||
v0Include.Name = &include.Name
|
||||
}
|
||||
if include.Component != "" {
|
||||
v0Include.Component = &include.Component
|
||||
}
|
||||
if include.Role != "" {
|
||||
var role pluginsv0alpha1.PluginMetaIncludeRole
|
||||
switch include.Role {
|
||||
case "Admin":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleAdmin
|
||||
case "Editor":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleEditor
|
||||
case "Viewer":
|
||||
role = pluginsv0alpha1.PluginMetaIncludeRoleViewer
|
||||
}
|
||||
v0Include.Role = &role
|
||||
}
|
||||
if include.Action != "" {
|
||||
v0Include.Action = &include.Action
|
||||
}
|
||||
if include.Path != "" {
|
||||
v0Include.Path = &include.Path
|
||||
}
|
||||
if include.AddToNav {
|
||||
v0Include.AddToNav = &include.AddToNav
|
||||
}
|
||||
if include.DefaultNav {
|
||||
v0Include.DefaultNav = &include.DefaultNav
|
||||
}
|
||||
if include.Icon != "" {
|
||||
v0Include.Icon = &include.Icon
|
||||
}
|
||||
meta.Includes = append(meta.Includes, v0Include)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Routes
|
||||
if len(jsonData.Routes) > 0 {
|
||||
meta.Routes = make([]pluginsv0alpha1.PluginMetaRoute, 0, len(jsonData.Routes))
|
||||
for _, route := range jsonData.Routes {
|
||||
v0Route := pluginsv0alpha1.PluginMetaRoute{}
|
||||
if route.Path != "" {
|
||||
v0Route.Path = &route.Path
|
||||
}
|
||||
if route.Method != "" {
|
||||
v0Route.Method = &route.Method
|
||||
}
|
||||
if route.URL != "" {
|
||||
v0Route.Url = &route.URL
|
||||
}
|
||||
if route.ReqRole != "" {
|
||||
reqRole := string(route.ReqRole)
|
||||
v0Route.ReqRole = &reqRole
|
||||
}
|
||||
if route.ReqAction != "" {
|
||||
v0Route.ReqAction = &route.ReqAction
|
||||
}
|
||||
if len(route.Headers) > 0 {
|
||||
headers := make([]string, 0, len(route.Headers))
|
||||
for _, header := range route.Headers {
|
||||
headers = append(headers, header.Name+": "+header.Content)
|
||||
}
|
||||
v0Route.Headers = headers
|
||||
}
|
||||
if len(route.URLParams) > 0 {
|
||||
v0Route.UrlParams = make([]pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams, 0, len(route.URLParams))
|
||||
for _, param := range route.URLParams {
|
||||
v0Param := pluginsv0alpha1.PluginMetaV0alpha1RouteUrlParams{}
|
||||
if param.Name != "" {
|
||||
v0Param.Name = ¶m.Name
|
||||
}
|
||||
if param.Content != "" {
|
||||
v0Param.Content = ¶m.Content
|
||||
}
|
||||
v0Route.UrlParams = append(v0Route.UrlParams, v0Param)
|
||||
}
|
||||
}
|
||||
if route.TokenAuth != nil {
|
||||
v0Route.TokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteTokenAuth{}
|
||||
if route.TokenAuth.Url != "" {
|
||||
v0Route.TokenAuth.Url = &route.TokenAuth.Url
|
||||
}
|
||||
if len(route.TokenAuth.Scopes) > 0 {
|
||||
v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes
|
||||
}
|
||||
if len(route.TokenAuth.Params) > 0 {
|
||||
v0Route.TokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.TokenAuth.Params {
|
||||
v0Route.TokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if route.JwtTokenAuth != nil {
|
||||
v0Route.JwtTokenAuth = &pluginsv0alpha1.PluginMetaV0alpha1RouteJwtTokenAuth{}
|
||||
if route.JwtTokenAuth.Url != "" {
|
||||
v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url
|
||||
}
|
||||
if len(route.JwtTokenAuth.Scopes) > 0 {
|
||||
v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes
|
||||
}
|
||||
if len(route.JwtTokenAuth.Params) > 0 {
|
||||
v0Route.JwtTokenAuth.Params = make(map[string]interface{})
|
||||
for k, v := range route.JwtTokenAuth.Params {
|
||||
v0Route.JwtTokenAuth.Params[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(route.Body) > 0 {
|
||||
var bodyMap map[string]interface{}
|
||||
if err := json.Unmarshal(route.Body, &bodyMap); err == nil {
|
||||
v0Route.Body = bodyMap
|
||||
}
|
||||
}
|
||||
meta.Routes = append(meta.Routes, v0Route)
|
||||
}
|
||||
}
|
||||
|
||||
// Map Extensions
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
|
||||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions := &pluginsv0alpha1.PluginMetaExtensions{}
|
||||
|
||||
if len(jsonData.Extensions.AddedLinks) > 0 {
|
||||
extensions.AddedLinks = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks))
|
||||
for _, link := range jsonData.Extensions.AddedLinks {
|
||||
v0Link := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedLinks{
|
||||
Targets: link.Targets,
|
||||
Title: link.Title,
|
||||
}
|
||||
if link.Description != "" {
|
||||
v0Link.Description = &link.Description
|
||||
}
|
||||
extensions.AddedLinks = append(extensions.AddedLinks, v0Link)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.AddedComponents) > 0 {
|
||||
extensions.AddedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents))
|
||||
for _, comp := range jsonData.Extensions.AddedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsAddedComponents{
|
||||
Targets: comp.Targets,
|
||||
Title: comp.Title,
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.AddedComponents = append(extensions.AddedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExposedComponents) > 0 {
|
||||
extensions.ExposedComponents = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
|
||||
for _, comp := range jsonData.Extensions.ExposedComponents {
|
||||
v0Comp := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExposedComponents{
|
||||
Id: comp.Id,
|
||||
}
|
||||
if comp.Title != "" {
|
||||
v0Comp.Title = &comp.Title
|
||||
}
|
||||
if comp.Description != "" {
|
||||
v0Comp.Description = &comp.Description
|
||||
}
|
||||
extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(jsonData.Extensions.ExtensionPoints) > 0 {
|
||||
extensions.ExtensionPoints = make([]pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints))
|
||||
for _, point := range jsonData.Extensions.ExtensionPoints {
|
||||
v0Point := pluginsv0alpha1.PluginMetaV0alpha1ExtensionsExtensionPoints{
|
||||
Id: point.Id,
|
||||
}
|
||||
if point.Title != "" {
|
||||
v0Point.Title = &point.Title
|
||||
}
|
||||
if point.Description != "" {
|
||||
v0Point.Description = &point.Description
|
||||
}
|
||||
extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point)
|
||||
}
|
||||
}
|
||||
|
||||
meta.Extensions = extensions
|
||||
}
|
||||
|
||||
// Map Roles
|
||||
if len(jsonData.Roles) > 0 {
|
||||
meta.Roles = make([]pluginsv0alpha1.PluginMetaRole, 0, len(jsonData.Roles))
|
||||
for _, role := range jsonData.Roles {
|
||||
v0Role := pluginsv0alpha1.PluginMetaRole{
|
||||
Grants: role.Grants,
|
||||
}
|
||||
if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole := &pluginsv0alpha1.PluginMetaV0alpha1RoleRole{}
|
||||
if role.Role.Name != "" {
|
||||
v0RoleRole.Name = &role.Role.Name
|
||||
}
|
||||
if role.Role.Description != "" {
|
||||
v0RoleRole.Description = &role.Role.Description
|
||||
}
|
||||
if len(role.Role.Permissions) > 0 {
|
||||
v0RoleRole.Permissions = make([]pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions))
|
||||
for _, perm := range role.Role.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1RoleRolePermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm)
|
||||
}
|
||||
}
|
||||
v0Role.Role = v0RoleRole
|
||||
}
|
||||
meta.Roles = append(meta.Roles, v0Role)
|
||||
}
|
||||
}
|
||||
|
||||
// Map IAM
|
||||
if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 {
|
||||
iam := &pluginsv0alpha1.PluginMetaIAM{
|
||||
Permissions: make([]pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)),
|
||||
}
|
||||
for _, perm := range jsonData.IAM.Permissions {
|
||||
v0Perm := pluginsv0alpha1.PluginMetaV0alpha1IAMPermissions{}
|
||||
if perm.Action != "" {
|
||||
v0Perm.Action = &perm.Action
|
||||
}
|
||||
if perm.Scope != "" {
|
||||
v0Perm.Scope = &perm.Scope
|
||||
}
|
||||
iam.Permissions = append(iam.Permissions, v0Perm)
|
||||
}
|
||||
meta.Iam = iam
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
@@ -22,16 +22,14 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
t.Run("returns cached plugin when available", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
|
||||
expectedSpec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedSpec
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
@@ -39,7 +37,7 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, expectedSpec, result.Meta)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
assert.Equal(t, defaultCoreTTL, result.TTL)
|
||||
})
|
||||
|
||||
@@ -60,16 +58,14 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
t.Run("ignores version parameter", func(t *testing.T) {
|
||||
provider := NewCoreProvider()
|
||||
|
||||
expectedSpec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedSpec
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
@@ -85,16 +81,14 @@ func TestCoreProvider_GetMeta(t *testing.T) {
|
||||
customTTL := 2 * time.Hour
|
||||
provider := NewCoreProviderWithTTL(customTTL)
|
||||
|
||||
expectedSpec := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider.mu.Lock()
|
||||
provider.loadedPlugins["test-plugin"] = expectedSpec
|
||||
provider.loadedPlugins["test-plugin"] = expectedMeta
|
||||
provider.initialized = true
|
||||
provider.mu.Unlock()
|
||||
|
||||
@@ -232,8 +226,8 @@ func TestCoreProvider_loadPlugins(t *testing.T) {
|
||||
if loaded {
|
||||
result, err := provider.GetMeta(ctx, "test-datasource", "1.0.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-datasource", result.Meta.PluginJson.Id)
|
||||
assert.Equal(t, "Test Datasource", result.Meta.PluginJson.Name)
|
||||
assert.Equal(t, "test-datasource", result.Meta.Id)
|
||||
assert.Equal(t, "Test Datasource", result.Meta.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLocalTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
// PluginAssetsCalculator is an interface for calculating plugin asset information.
|
||||
// LocalProvider requires this to calculate loading strategy and module hash.
|
||||
type PluginAssetsCalculator interface {
|
||||
LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy
|
||||
ModuleHash(ctx context.Context, p pluginstore.Plugin) string
|
||||
}
|
||||
|
||||
// LocalProvider retrieves plugin metadata for locally installed plugins.
|
||||
// It uses the plugin store to access plugins that have already been loaded.
|
||||
type LocalProvider struct {
|
||||
store pluginstore.Store
|
||||
pluginAssets PluginAssetsCalculator
|
||||
}
|
||||
|
||||
// NewLocalProvider creates a new LocalProvider for locally installed plugins.
|
||||
// pluginAssets is required for calculating loading strategy and module hash.
|
||||
func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider {
|
||||
return &LocalProvider{
|
||||
store: pluginStore,
|
||||
pluginAssets: pluginAssets,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMeta retrieves plugin metadata for locally installed plugins.
|
||||
func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
plugin, exists := p.store.Plugin(ctx, pluginID)
|
||||
if !exists {
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
|
||||
loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin)
|
||||
moduleHash := p.pluginAssets.ModuleHash(ctx, plugin)
|
||||
|
||||
spec := pluginStorePluginToPluginMetaSpec(plugin, loadingStrategy, moduleHash)
|
||||
return &Result{
|
||||
Meta: spec,
|
||||
TTL: defaultLocalTTL,
|
||||
}, nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const (
|
||||
|
||||
// cachedMeta represents a cached metadata entry with expiration time
|
||||
type cachedMeta struct {
|
||||
meta pluginsv0alpha1.PluginMetaSpec
|
||||
meta pluginsv0alpha1.PluginMetaJSONData
|
||||
ttl time.Duration
|
||||
expiresAt time.Time
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func (pm *ProviderManager) GetMeta(ctx context.Context, pluginID, version string
|
||||
if err == nil {
|
||||
// Don't cache results with a zero TTL
|
||||
if result.TTL == 0 {
|
||||
return result, nil
|
||||
continue
|
||||
}
|
||||
|
||||
pm.cacheMu.Lock()
|
||||
|
||||
@@ -35,12 +35,10 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns cached result when available and not expired", func(t *testing.T) {
|
||||
cachedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
cachedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider := &mockProvider{
|
||||
@@ -62,10 +60,8 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
|
||||
provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "different"},
|
||||
},
|
||||
TTL: time.Hour,
|
||||
Meta: pluginsv0alpha1.PluginMetaJSONData{Id: "different"},
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -77,12 +73,10 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("fetches from provider when not cached", func(t *testing.T) {
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedTTL := 2 * time.Hour
|
||||
|
||||
@@ -113,16 +107,19 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
assert.Equal(t, expectedTTL, cached.ttl)
|
||||
})
|
||||
|
||||
t.Run("does not cache result with zero TTL", func(t *testing.T) {
|
||||
zeroTTLMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Zero TTL Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) {
|
||||
zeroTTLMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Zero TTL Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider := &mockProvider{
|
||||
provider1 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: zeroTTLMeta,
|
||||
@@ -130,30 +127,37 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
provider2 := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
return &Result{
|
||||
Meta: expectedMeta,
|
||||
TTL: time.Hour,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
pm := NewProviderManager(provider)
|
||||
pm := NewProviderManager(provider1, provider2)
|
||||
|
||||
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, zeroTTLMeta, result.Meta)
|
||||
assert.Equal(t, time.Duration(0), result.TTL)
|
||||
assert.Equal(t, expectedMeta, result.Meta)
|
||||
|
||||
pm.cacheMu.RLock()
|
||||
_, exists := pm.cache["test-plugin:1.0.0"]
|
||||
cached, exists := pm.cache["test-plugin:1.0.0"]
|
||||
pm.cacheMu.RUnlock()
|
||||
|
||||
assert.False(t, exists, "zero TTL results should not be cached")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, expectedMeta, cached.meta)
|
||||
assert.Equal(t, time.Hour, cached.ttl)
|
||||
})
|
||||
|
||||
t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) {
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
@@ -225,19 +229,15 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("skips expired cache entries", func(t *testing.T) {
|
||||
expiredMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Expired Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expiredMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Expired Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Test Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
callCount := 0
|
||||
@@ -272,19 +272,15 @@ func TestProviderManager_GetMeta(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("uses first successful provider", func(t *testing.T) {
|
||||
expectedMeta1 := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 1 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta1 := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 1 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
expectedMeta2 := pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJson: pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 2 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
},
|
||||
expectedMeta2 := pluginsv0alpha1.PluginMetaJSONData{
|
||||
Id: "test-plugin",
|
||||
Name: "Provider 2 Plugin",
|
||||
Type: pluginsv0alpha1.PluginMetaJSONDataTypeDatasource,
|
||||
}
|
||||
|
||||
provider1 := &mockProvider{
|
||||
@@ -335,9 +331,9 @@ func TestProviderManager_Run(t *testing.T) {
|
||||
|
||||
func TestProviderManager_cleanupExpired(t *testing.T) {
|
||||
t.Run("removes expired entries", func(t *testing.T) {
|
||||
validMeta := pluginsv0alpha1.PluginMetaSpec{PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "valid"}}
|
||||
expiredMeta1 := pluginsv0alpha1.PluginMetaSpec{PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"}}
|
||||
expiredMeta2 := pluginsv0alpha1.PluginMetaSpec{PluginJson: pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"}}
|
||||
validMeta := pluginsv0alpha1.PluginMetaJSONData{Id: "valid"}
|
||||
expiredMeta1 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired1"}
|
||||
expiredMeta2 := pluginsv0alpha1.PluginMetaJSONData{Id: "expired2"}
|
||||
|
||||
provider := &mockProvider{
|
||||
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
|
||||
|
||||
@@ -14,14 +14,14 @@ var (
|
||||
|
||||
// Result contains plugin metadata along with its recommended TTL.
|
||||
type Result struct {
|
||||
Meta pluginsv0alpha1.PluginMetaSpec
|
||||
Meta pluginsv0alpha1.PluginMetaJSONData
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
// Provider is used for retrieving plugin metadata.
|
||||
type Provider interface {
|
||||
// GetMeta retrieves plugin metadata for the given plugin ID and version.
|
||||
// Returns the Result containing the PluginMetaSpec and its recommended TTL.
|
||||
// Returns the Result containing the PluginMetaJSONData and its recommended TTL.
|
||||
// If the plugin is not found, returns ErrMetaNotFound.
|
||||
GetMeta(ctx context.Context, pluginID, version string) (*Result, error)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func (s *PluginMetaStorage) List(ctx context.Context, options *internalversion.L
|
||||
continue
|
||||
}
|
||||
|
||||
pluginMeta := createPluginMetaFromSpec(result.Meta, plugin.Name, plugin.Namespace)
|
||||
pluginMeta := createPluginMetaFromPluginMetaJSONData(result.Meta, plugin.Name, plugin.Namespace)
|
||||
metaItems = append(metaItems, *pluginMeta)
|
||||
}
|
||||
|
||||
@@ -174,17 +174,19 @@ func (s *PluginMetaStorage) Get(ctx context.Context, name string, options *metav
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err))
|
||||
}
|
||||
|
||||
return createPluginMetaFromSpec(result.Meta, name, ns.Value), nil
|
||||
return createPluginMetaFromPluginMetaJSONData(result.Meta, name, ns.Value), nil
|
||||
}
|
||||
|
||||
// createPluginMetaFromSpec creates a PluginMeta k8s object from PluginMetaSpec and plugin metadata.
|
||||
func createPluginMetaFromSpec(spec pluginsv0alpha1.PluginMetaSpec, name, namespace string) *pluginsv0alpha1.PluginMeta {
|
||||
// createPluginMetaFromPluginMetaJSONData creates a PluginMeta k8s object from PluginMetaJSONData and plugin metadata.
|
||||
func createPluginMetaFromPluginMetaJSONData(pluginJSON pluginsv0alpha1.PluginMetaJSONData, name, namespace string) *pluginsv0alpha1.PluginMeta {
|
||||
pluginMeta := &pluginsv0alpha1.PluginMeta{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: spec,
|
||||
Spec: pluginsv0alpha1.PluginMetaSpec{
|
||||
PluginJSON: pluginJSON,
|
||||
},
|
||||
}
|
||||
|
||||
// Set the GroupVersionKind
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
; Run locally unified storage with SQLite to test
|
||||
; new API registration changes
|
||||
app_mode = development
|
||||
target = all
|
||||
|
||||
[log]
|
||||
level = debug
|
||||
|
||||
[server]
|
||||
; HTTPS is required for kubectl (but HTTP works for testing with curl)
|
||||
protocol = https
|
||||
http_port = 1111
|
||||
|
||||
[feature_toggles]
|
||||
; Enable the apiextensions feature
|
||||
apiExtensions = true
|
||||
; Enable unified storage globally
|
||||
unifiedStorage = true
|
||||
; Enable search indexing for unified storage
|
||||
unifiedStorageSearch = true
|
||||
; Enable the grafana-apiserver explicitly
|
||||
grafanaAPIServer = true
|
||||
; Enable K8s aggregator for API discovery aggregation
|
||||
; NOTE: This is an enterprise-only feature that requires TLS certificates
|
||||
; This will surface the new registered group APIs to the `/apis` endpoint.
|
||||
kubernetesAggregator = true
|
||||
|
||||
[grafana-apiserver]
|
||||
; Use unified storage backed by SQL (uses your Grafana database)
|
||||
storage_type = unified
|
||||
; Certificates for the Kubernetes aggregator (generated by hack/make-aggregator-pki.sh)
|
||||
proxy_client_cert_file = data/grafana-aggregator/client.crt
|
||||
proxy_client_key_file = data/grafana-aggregator/client.key
|
||||
|
||||
; Configure dashboards to use unified storage
|
||||
[unified_storage.dashboards.dashboard.grafana.app]
|
||||
dualWriterMode = 5
|
||||
|
||||
; Configure folders to use unified storage (required for dashboards)
|
||||
[unified_storage.folders.folder.grafana.app]
|
||||
dualWriterMode = 5
|
||||
|
||||
[database]
|
||||
; SQLite database for testing
|
||||
type = sqlite3
|
||||
path = grafana.db
|
||||
high_availability = false
|
||||
|
||||
; Will only be used for the MT grafana
|
||||
; apiextensions service
|
||||
; [auth.extended_jwt]
|
||||
; enabled = true
|
||||
; jwks_url = "http://localhost:6481/jwks"
|
||||
@@ -14,10 +14,10 @@ weight: 400
|
||||
The Grafana Cloud Migration Assistant, generally available from Grafana v12.0, automatically migrates resources from your Grafana OSS/Enterprise instance to Grafana Cloud. It provides the following functionality:
|
||||
|
||||
- Securely connect your self-managed instance to a Grafana Cloud instance.
|
||||
- Migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps.
|
||||
- Seamlessly migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps.
|
||||
- View the migration status of your resources in real-time.
|
||||
|
||||
Some benefits of the migration assistant are:
|
||||
Some of the benefits of the migration assistant are:
|
||||
|
||||
Ease of use
|
||||
: Follow the steps provided by the UI to easily migrate all your resources to Grafana Cloud without using Grafana APIs or scripts.
|
||||
@@ -44,7 +44,7 @@ The following resources are supported by the migration assistant:
|
||||
|
||||
To use the Grafana migration assistant, you need:
|
||||
|
||||
- A self-managed Grafana instance version v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles).
|
||||
- Grafana v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles).
|
||||
- A [Grafana Cloud Stack](https://grafana.com/docs/grafana-cloud/get-started/) you intend to migrate your resources to.
|
||||
- [`Admin`](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/) access to the Grafana Cloud Stack. To check your access level, go to `https://grafana.com/orgs/<YOUR-ORG-NAME>/members`.
|
||||
- [Grafana server administrator](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators) access to your existing Grafana OSS/Enterprise instance. To check your access level, go to `https://<GRAFANA-ONPREM-URL>/admin/users`.
|
||||
@@ -64,7 +64,7 @@ In Grafana Enterprise, the server administrator has access to the migration assi
|
||||
|
||||
### Grant access in Grafana Enterprise
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
{{< admonition type="important">}}
|
||||
You must [configure RBAC](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/configure-rbac/) before you can grant other administrators access to the Grafana Migration Assistant.
|
||||
{{< /admonition >}}
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `grafanaAssistantInProfilesDrilldown` | Enables integration with Grafana Assistant in Profiles Drilldown | Yes |
|
||||
| `sharingDashboardImage` | Enables image sharing functionality for dashboards | Yes |
|
||||
| `tabularNumbers` | Use fixed-width numbers globally in the UI | |
|
||||
| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | Yes |
|
||||
| `tempoSearchBackendMigration` | Run search queries through the tempo backend | |
|
||||
|
||||
## Public preview feature toggles
|
||||
@@ -96,9 +95,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `localeFormatPreference` | Specifies the locale so the correct format for numbers and dates can be shown |
|
||||
| `logsPanelControls` | Enables a control component for the logs panel in Explore |
|
||||
| `interactiveLearning` | Enables the interactive learning app |
|
||||
| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker |
|
||||
| `newVizSuggestions` | Enable new visualization suggestions |
|
||||
| `preventPanelChromeOverflow` | Restrict PanelChrome contents with overflow: hidden; |
|
||||
| `newPanelPadding` | Increases panel padding globally |
|
||||
| `transformationsEmptyPlaceholder` | Show transformation quick-start cards in empty transformations state |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
@@ -1912,6 +1912,11 @@
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 3
|
||||
|
||||
@@ -407,6 +407,7 @@ github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye
|
||||
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
|
||||
github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA=
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw=
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
@@ -846,8 +847,10 @@ github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkM
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
|
||||
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
|
||||
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
|
||||
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
|
||||
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
|
||||
@@ -896,6 +899,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagI
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.279.0/go.mod h1:/7oGN6Z7DGTGaLHhgIYrRr6Wvmdsb3BLw5hL4Kbjy88=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.280.0/go.mod h1:Z15Wiq3c4I0tzHYrLYpOqrO8u3+2RJ+HN2Q9uiZTILA=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0=
|
||||
github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA=
|
||||
github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c=
|
||||
@@ -923,6 +927,7 @@ github.com/grafana/nanogit v0.0.0-20250616082354-5e94194d02ed/go.mod h1:OIAAKNgG
|
||||
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5 h1:MAQ2B0cu0V1S91ZjVa7NomNZFjaR2SmdtvdwhqBtyhU=
|
||||
github.com/grafana/nanogit v0.0.0-20250619160700-ebf70d342aa5/go.mod h1:tN93IZUaAmnSWgL0IgnKdLv6DNeIhTJGvl1wvQMrWco=
|
||||
github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
|
||||
github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250331083058-4563aec7a975 h1:4/BZkGObFWZf4cLbE2Vqg/1VTz67Q0AJ7LHspWLKJoQ=
|
||||
@@ -939,6 +944,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
@@ -1324,6 +1330,7 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
|
||||
github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8=
|
||||
@@ -1375,6 +1382,7 @@ github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiy
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/schollz/progressbar/v3 v3.14.6 h1:GyjwcWBAf+GFDMLziwerKvpuS7ZF+mNTAXIB2aspiZs=
|
||||
github.com/schollz/progressbar/v3 v3.14.6/go.mod h1:Nrzpuw3Nl0srLY0VlTvC4V6RL50pcEymjy6qyJAaLa0=
|
||||
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
|
||||
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
|
||||
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
|
||||
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
|
||||
@@ -1932,6 +1940,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/plugin-types": "^0.0.48",
|
||||
"@grafana/scenes": "6.38.0",
|
||||
"@rollup/plugin-node-resolve": "16.0.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
|
||||
@@ -921,4 +921,3 @@ export {
|
||||
} from './rbac/rbac';
|
||||
|
||||
export { type UserStorage } from './types/userStorage';
|
||||
export { type PluginMetasResponse, type PluginMetasSpec } from './types/plugin';
|
||||
|
||||
@@ -381,11 +381,6 @@ export class PanelPlugin<
|
||||
const appender = builder.getListAppender<TOptions, TFieldConfigOptions>({
|
||||
pluginId: this.meta.id,
|
||||
name: this.meta.name,
|
||||
options: {},
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = supplier(builder.dataSummary);
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import { PreferredVisualisationType } from '../../types/data';
|
||||
import { DataFrame, FieldType } from '../../types/dataFrame';
|
||||
import { DataFrameType } from '../../types/dataFrameTypes';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface PanelDataSummary {
|
||||
hasData?: boolean;
|
||||
rowCountTotal: number;
|
||||
/** max number of rows in any given dataframe in the panel data */
|
||||
rowCountMax: number;
|
||||
frameCount: number;
|
||||
fieldCount: number;
|
||||
/** max number of fields in any given dataframe in the panel data */
|
||||
fieldCountMax: number;
|
||||
/** given a field type, return the number of fields across all dataframes which match this type */
|
||||
fieldCountByType: (type: FieldType) => number;
|
||||
/** returns true if any fields in any frames match the field type */
|
||||
hasFieldType: (type: FieldType) => boolean;
|
||||
/* returns true if any of the frames in this panel data summary have the type */
|
||||
hasDataFrameType: (type: DataFrameType) => boolean;
|
||||
/* returns true if any of the frames in this panel data summary have the type */
|
||||
hasPreferredVisualisationType: (type: PreferredVisualisationType) => boolean;
|
||||
|
||||
/** pass along a reference to the DataFrame array in case it's needed by the plugin */
|
||||
rawFrames?: DataFrame[];
|
||||
/** The first frame that set's this value */
|
||||
preferredVisualisationType?: PreferredVisualisationType;
|
||||
|
||||
/* --- DEPRECATED FIELDS BELOW --- */
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
|
||||
@@ -31,114 +23,60 @@ export interface PanelDataSummary {
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
|
||||
stringFieldCount: number;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
|
||||
hasTimeField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
hasNumberField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
hasTimeField?: boolean;
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */
|
||||
hasStringField?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
class PanelDataSummaryImpl implements PanelDataSummary {
|
||||
public rowCountTotal = 0;
|
||||
/** max number of rows in any single dataframe in the panel data */
|
||||
public rowCountMax = 0;
|
||||
public fieldCount = 0;
|
||||
/** max number of fields in any single dataframe in the panel data */
|
||||
public fieldCountMax = 0;
|
||||
|
||||
private countByType: Partial<Record<FieldType, number>> = {};
|
||||
private preferredVisualisationTypes: Set<PreferredVisualisationType> = new Set<PreferredVisualisationType>();
|
||||
private dataFrameTypes: Set<DataFrameType> = new Set<DataFrameType>();
|
||||
|
||||
public get hasData(): boolean {
|
||||
return this.rowCountTotal > 0;
|
||||
}
|
||||
|
||||
public get frameCount(): number {
|
||||
return this.rawFrames?.length ?? 0;
|
||||
}
|
||||
|
||||
constructor(public rawFrames?: DataFrame[]) {
|
||||
this._processFrames();
|
||||
}
|
||||
|
||||
private _processFrames() {
|
||||
for (const frame of this.rawFrames ?? []) {
|
||||
this.rowCountTotal += frame.length;
|
||||
|
||||
if (frame.meta?.preferredVisualisationType) {
|
||||
this.preferredVisualisationTypes.add(frame.meta.preferredVisualisationType);
|
||||
}
|
||||
if (frame.meta?.type) {
|
||||
this.dataFrameTypes.add(frame.meta.type);
|
||||
}
|
||||
|
||||
for (const field of frame.fields) {
|
||||
this.fieldCount++;
|
||||
this.countByType[field.type] = (this.countByType[field.type] || 0) + 1;
|
||||
}
|
||||
|
||||
if (frame.length > this.rowCountMax) {
|
||||
this.rowCountMax = frame.length;
|
||||
}
|
||||
if (frame.fields.length > this.fieldCountMax) {
|
||||
this.fieldCountMax = frame.fields.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fieldCountByType(type: FieldType): number {
|
||||
return this.countByType[type] ?? 0;
|
||||
}
|
||||
|
||||
public hasFieldType(type: FieldType): boolean {
|
||||
return this.fieldCountByType(type) > 0;
|
||||
}
|
||||
|
||||
public hasPreferredVisualisationType(type: PreferredVisualisationType): boolean {
|
||||
return this.preferredVisualisationTypes.has(type);
|
||||
}
|
||||
|
||||
public hasDataFrameType(type: DataFrameType): boolean {
|
||||
return this.dataFrameTypes.has(type);
|
||||
}
|
||||
|
||||
/**** DEPRECATED ****/
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
|
||||
public get numberFieldCount(): number {
|
||||
return this.fieldCountByType(FieldType.number);
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.time) */
|
||||
public get timeFieldCount(): number {
|
||||
return this.fieldCountByType(FieldType.time);
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
|
||||
public get stringFieldCount() {
|
||||
return this.fieldCountByType(FieldType.string);
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
|
||||
public get hasTimeField() {
|
||||
return this.fieldCountByType(FieldType.time) > 0;
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
|
||||
public get hasNumberField() {
|
||||
return this.fieldCountByType(FieldType.number) > 0;
|
||||
}
|
||||
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */
|
||||
public get hasStringField() {
|
||||
return this.fieldCountByType(FieldType.string) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* given a list of dataframes, summarize attributes of those frames for features like suggestions.
|
||||
* @param frames - dataframes to summarize
|
||||
* @returns summary of the dataframes
|
||||
*/
|
||||
export function getPanelDataSummary(frames?: DataFrame[]): PanelDataSummary {
|
||||
return new PanelDataSummaryImpl(frames);
|
||||
export function getPanelDataSummary(frames: DataFrame[] = []): PanelDataSummary {
|
||||
let rowCountTotal = 0;
|
||||
let rowCountMax = 0;
|
||||
let fieldCount = 0;
|
||||
const countByType: Partial<Record<FieldType, number>> = {};
|
||||
let preferredVisualisationType: PreferredVisualisationType | undefined;
|
||||
|
||||
for (const frame of frames) {
|
||||
rowCountTotal += frame.length;
|
||||
|
||||
if (frame.meta?.preferredVisualisationType) {
|
||||
preferredVisualisationType = frame.meta.preferredVisualisationType;
|
||||
}
|
||||
|
||||
for (const field of frame.fields) {
|
||||
fieldCount++;
|
||||
countByType[field.type] = (countByType[field.type] || 0) + 1;
|
||||
}
|
||||
|
||||
if (frame.length > rowCountMax) {
|
||||
rowCountMax = frame.length;
|
||||
}
|
||||
}
|
||||
|
||||
const fieldCountByType = (f: FieldType) => countByType[f] ?? 0;
|
||||
|
||||
return {
|
||||
rowCountTotal,
|
||||
rowCountMax,
|
||||
fieldCount,
|
||||
preferredVisualisationType,
|
||||
frameCount: frames.length,
|
||||
hasData: rowCountTotal > 0,
|
||||
hasFieldType: (f: FieldType) => fieldCountByType(f) > 0,
|
||||
fieldCountByType,
|
||||
// deprecated
|
||||
numberFieldCount: fieldCountByType(FieldType.number),
|
||||
timeFieldCount: fieldCountByType(FieldType.time),
|
||||
stringFieldCount: fieldCountByType(FieldType.string),
|
||||
hasTimeField: fieldCountByType(FieldType.time) > 0,
|
||||
hasNumberField: fieldCountByType(FieldType.number) > 0,
|
||||
hasStringField: fieldCountByType(FieldType.string) > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ interface IndexOptions {
|
||||
asPercentile: boolean;
|
||||
}
|
||||
|
||||
const defaultNumericVizOptions: ReduceOptions = {
|
||||
const defaultReduceOptions: ReduceOptions = {
|
||||
reducer: ReducerID.sum,
|
||||
};
|
||||
|
||||
@@ -149,10 +149,10 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
|
||||
|
||||
switch (mode) {
|
||||
case CalculateFieldMode.ReduceRow:
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultNumericVizOptions), data);
|
||||
creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.CumulativeFunctions:
|
||||
creator = getCumulativeCreator(defaults(options.cumulative, defaultNumericVizOptions), data);
|
||||
creator = getCumulativeCreator(defaults(options.cumulative, defaultReduceOptions), data);
|
||||
break;
|
||||
case CalculateFieldMode.WindowFunctions:
|
||||
creator = getWindowCreator(defaults(options.window, defaultWindowOptions), data);
|
||||
|
||||
+6
-2
@@ -463,6 +463,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
kubernetesAggregatorCapTokenAuth?: boolean;
|
||||
/**
|
||||
* Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration
|
||||
*/
|
||||
apiExtensions?: boolean;
|
||||
/**
|
||||
* Enable groupBy variable support in scenes dashboards
|
||||
*/
|
||||
groupByVariable?: boolean;
|
||||
@@ -1091,7 +1095,7 @@ export interface FeatureToggles {
|
||||
graphiteBackendMode?: boolean;
|
||||
/**
|
||||
* Enables the updated Azure Monitor resource picker
|
||||
* @default true
|
||||
* @default false
|
||||
*/
|
||||
azureResourcePickerUpdates?: boolean;
|
||||
/**
|
||||
@@ -1150,7 +1154,7 @@ export interface FeatureToggles {
|
||||
pluginStoreServiceLoading?: boolean;
|
||||
/**
|
||||
* Increases panel padding globally
|
||||
* @default true
|
||||
* @default false
|
||||
*/
|
||||
newPanelPadding?: boolean;
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { PluginSchema } from '@grafana/plugin-types/plugin-schema';
|
||||
|
||||
import { KeyValue } from './data';
|
||||
import { IconName } from './icon';
|
||||
|
||||
@@ -268,32 +266,3 @@ export class GrafanaPlugin<T extends PluginMeta = PluginMeta> {
|
||||
this.meta = {} as T;
|
||||
}
|
||||
}
|
||||
export interface PluginMetasResponse {
|
||||
items: PluginMetasItem[];
|
||||
}
|
||||
|
||||
export interface PluginMetasItem {
|
||||
spec: PluginMetasSpec;
|
||||
}
|
||||
|
||||
export interface PluginMetasSpec {
|
||||
pluginJson: PluginSchema;
|
||||
module: PluginMetasModule;
|
||||
baseURL: string;
|
||||
signature: PluginMetasSignature;
|
||||
angular: PluginMetasAngular;
|
||||
translations?: PluginSchema['languages'];
|
||||
}
|
||||
|
||||
export interface PluginMetasAngular {
|
||||
detected: boolean;
|
||||
}
|
||||
|
||||
export interface PluginMetasModule {
|
||||
path: string;
|
||||
loadingStrategy: PluginLoadingStrategy;
|
||||
}
|
||||
|
||||
export interface PluginMetasSignature {
|
||||
status: PluginSignatureStatus;
|
||||
}
|
||||
|
||||
@@ -75,9 +75,7 @@ export interface VisualizationSuggestion<TOptions extends unknown = {}, TFieldCo
|
||||
* mutate the suggestion object which is passed in as the first argument.
|
||||
*/
|
||||
previewModifier?: (suggestion: VisualizationSuggestion<TOptions, TFieldConfig>) => void;
|
||||
/** @deprecated this will no longer be supported in the new Suggestions UI. */
|
||||
icon?: string;
|
||||
/** @deprecated this will no longer be supported in the new Suggestions UI. */
|
||||
imgSrc?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
UnifiedAlertingConfig,
|
||||
GrafanaConfig,
|
||||
CurrentUserDTO,
|
||||
PluginMetasSpec,
|
||||
} from '@grafana/data';
|
||||
|
||||
/**
|
||||
@@ -267,11 +266,6 @@ export class GrafanaBootConfig {
|
||||
listScopesEndpoint = '';
|
||||
|
||||
openFeatureContext: Record<string, unknown> = {};
|
||||
plugins: Record<'apps' | 'panels' | 'datasources', Record<string, PluginMetasSpec>> = {
|
||||
apps: {},
|
||||
datasources: {},
|
||||
panels: {},
|
||||
};
|
||||
|
||||
constructor(
|
||||
options: BootData['settings'] & {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { dateTime, makeTimeRange, TimeRange, BootData } from '@grafana/data';
|
||||
import { dateTime, makeTimeRange, TimeRange } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { TimeRangeProvider } from './TimeRangeContext';
|
||||
@@ -152,58 +152,6 @@ it('does not submit wrapping forms', async () => {
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows CTRL+Z in zoom out tooltip when feature flag is disabled', async () => {
|
||||
window.grafanaBootData = {
|
||||
settings: {
|
||||
featureToggles: {
|
||||
newTimeRangeZoomShortcuts: false,
|
||||
},
|
||||
},
|
||||
} as BootData;
|
||||
|
||||
render(
|
||||
<TimeRangePicker
|
||||
onChangeTimeZone={() => {}}
|
||||
onChange={(value) => {}}
|
||||
value={value}
|
||||
onMoveBackward={() => {}}
|
||||
onMoveForward={() => {}}
|
||||
onZoom={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const zoomButton = screen.getByLabelText('Zoom out time range');
|
||||
await userEvent.hover(zoomButton);
|
||||
|
||||
expect(await screen.findByText(/CTRL\+Z/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows t - in zoom out tooltip when feature flag is enabled', async () => {
|
||||
window.grafanaBootData = {
|
||||
settings: {
|
||||
featureToggles: {
|
||||
newTimeRangeZoomShortcuts: true,
|
||||
},
|
||||
},
|
||||
} as BootData;
|
||||
|
||||
render(
|
||||
<TimeRangePicker
|
||||
onChangeTimeZone={() => {}}
|
||||
onChange={(value) => {}}
|
||||
value={value}
|
||||
onMoveBackward={() => {}}
|
||||
onMoveForward={() => {}}
|
||||
onZoom={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const zoomButton = screen.getByLabelText('Zoom out time range');
|
||||
await userEvent.hover(zoomButton);
|
||||
|
||||
expect(await screen.findByText(/t -/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('TimePickerTooltip', () => {
|
||||
beforeAll(() => {
|
||||
const mockIntl = {
|
||||
|
||||
@@ -19,7 +19,6 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { getFeatureToggle } from '../../utils/featureToggle';
|
||||
import { ButtonGroup } from '../Button/ButtonGroup';
|
||||
import { getModalStyles } from '../Modal/getModalStyles';
|
||||
import { getPortalContainer } from '../Portal/Portal';
|
||||
@@ -244,22 +243,13 @@ export function TimeRangePicker(props: TimeRangePickerProps) {
|
||||
|
||||
TimeRangePicker.displayName = 'TimeRangePicker';
|
||||
|
||||
const ZoomOutTooltip = () => {
|
||||
const newShortcuts = getFeatureToggle('newTimeRangeZoomShortcuts');
|
||||
return (
|
||||
<>
|
||||
{newShortcuts ? (
|
||||
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip-new">
|
||||
Time range zoom out <br /> t -
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip">
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</Trans>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const ZoomOutTooltip = () => (
|
||||
<>
|
||||
<Trans i18nKey="time-picker.range-picker.zoom-out-tooltip">
|
||||
Time range zoom out <br /> CTRL+Z
|
||||
</Trans>
|
||||
</>
|
||||
);
|
||||
|
||||
export const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => {
|
||||
const styles = useStyles2(getLabelStyles);
|
||||
|
||||
@@ -29,7 +29,7 @@ const cursorDefaults: Cursor = {
|
||||
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
|
||||
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
|
||||
|
||||
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number; isTimeRangePending?: boolean };
|
||||
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number };
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
readonly uid = Math.random().toString(36).slice(2);
|
||||
|
||||
+1
-15
@@ -137,7 +137,7 @@ describe('XAxisInteractionAreaPlugin', () => {
|
||||
expect(mockQueryZoom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set isPanning state during drag and mark isTimeRangePending on mouseup', () => {
|
||||
it('should set isPanning state during drag and clear on mouseup', () => {
|
||||
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
|
||||
|
||||
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
|
||||
@@ -153,20 +153,6 @@ describe('XAxisInteractionAreaPlugin', () => {
|
||||
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 350, bubbles: true }));
|
||||
|
||||
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({
|
||||
isPanning: true,
|
||||
min: expectedRange.from,
|
||||
max: expectedRange.to,
|
||||
isTimeRangePending: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear isPanning state immediately for small drags below threshold', () => {
|
||||
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
|
||||
|
||||
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 402, bubbles: true }));
|
||||
|
||||
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ isPanning: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,14 +96,11 @@ export const setupXAxisPan = (
|
||||
|
||||
xAxisEl.style.cursor = 'grab';
|
||||
|
||||
const isSignificantDrag = Math.abs(dragPixels) >= MIN_PAN_DIST;
|
||||
config.setState({ isPanning: false });
|
||||
|
||||
if (isSignificantDrag) {
|
||||
if (Math.abs(dragPixels) >= MIN_PAN_DIST) {
|
||||
const newRange = calculatePanRange(startMin, startMax, dragPixels, u.bbox.width);
|
||||
config.setState({ isPanning: true, min: newRange.from, max: newRange.to, isTimeRangePending: true });
|
||||
queryZoom(newRange);
|
||||
} else {
|
||||
config.setState({ isPanning: false });
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -202,11 +200,6 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response.
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "GetDataSourceByUID"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID())
|
||||
|
||||
if err != nil {
|
||||
@@ -238,11 +231,6 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "DeleteDataSourceByUID"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
|
||||
if uid == "" {
|
||||
@@ -373,11 +361,6 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error {
|
||||
// 409: conflictError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "AddDataSource"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
cmd := datasources.AddDataSourceCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
@@ -495,10 +478,6 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
|
||||
// 409: conflictError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "UpdateDataSourceByUID"), time.Since(start).Seconds())
|
||||
}()
|
||||
cmd := datasources.UpdateDataSourceCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -17,7 +16,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
@@ -83,19 +81,6 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
|
||||
}, mockSQLStore)
|
||||
}
|
||||
|
||||
// setupDsConfigMetrics creates and registers the prometheus metrics needed for HTTPServer tests
|
||||
// that call methods using dsConfigHandlerRequestsDuration.
|
||||
func setupDsConfigHandlerMetrics() (prometheus.Registerer, *prometheus.HistogramVec) {
|
||||
promRegister := prometheus.NewRegistry()
|
||||
dsConfigHandlerRequestsDuration := metricutil.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "grafana",
|
||||
Name: "ds_config_handler_requests_duration_seconds",
|
||||
Help: "Duration of requests handled by datasource configuration handlers",
|
||||
}, []string{"code_path", "handler"})
|
||||
promRegister.MustRegister(dsConfigHandlerRequestsDuration)
|
||||
return promRegister, dsConfigHandlerRequestsDuration
|
||||
}
|
||||
|
||||
// Adding data sources with invalid URLs should lead to an error.
|
||||
func TestAddDataSource_InvalidURL(t *testing.T) {
|
||||
sc := setupScenarioContext(t, "/api/datasources")
|
||||
@@ -103,7 +88,6 @@ func TestAddDataSource_InvalidURL(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
|
||||
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
|
||||
@@ -134,7 +118,6 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources")
|
||||
|
||||
@@ -160,7 +143,6 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources")
|
||||
|
||||
@@ -193,7 +175,6 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
sc := setupScenarioContext(t, "/api/datasources/1234")
|
||||
|
||||
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
|
||||
@@ -218,7 +199,6 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{},
|
||||
Cfg: setting.NewCfg(),
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
sc := setupScenarioContext(t, "/api/datasources/1234")
|
||||
|
||||
hs.Cfg.AuthProxy.Enabled = true
|
||||
@@ -256,7 +236,6 @@ func TestAddDataSourceTeamHTTPHeaders(t *testing.T) {
|
||||
ExpectedErr: nil,
|
||||
},
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID))
|
||||
hs.Cfg.AuthProxy.Enabled = true
|
||||
|
||||
@@ -310,7 +289,6 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources/1234")
|
||||
|
||||
@@ -451,7 +429,6 @@ func TestAPI_datasources_AccessControl(t *testing.T) {
|
||||
hs.DataSourcesService = &dataSourcesServiceMock{expectedDatasource: &datasources.DataSource{}}
|
||||
hs.accesscontrolService = actest.FakeService{}
|
||||
hs.Live = newTestLive(t, hs.SQLStore)
|
||||
hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics()
|
||||
})
|
||||
|
||||
for _, url := range tt.urls {
|
||||
|
||||
+21
-28
@@ -203,28 +203,27 @@ type HTTPServer struct {
|
||||
pluginsCDNService *pluginscdn.Service
|
||||
managedPluginsService managedplugins.Manager
|
||||
|
||||
userService user.Service
|
||||
tempUserService tempUser.Service
|
||||
loginAttemptService loginAttempt.Service
|
||||
orgService org.Service
|
||||
orgDeletionService org.DeletionService
|
||||
TeamService team.Service
|
||||
accesscontrolService accesscontrol.Service
|
||||
annotationsRepo annotations.Repository
|
||||
tagService tag.Service
|
||||
oauthTokenService oauthtoken.OAuthTokenService
|
||||
statsService stats.Service
|
||||
authnService authn.Service
|
||||
starApi *starApi.API
|
||||
promRegister prometheus.Registerer
|
||||
promGatherer prometheus.Gatherer
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
namespacer request.NamespaceMapper
|
||||
anonService anonymous.Service
|
||||
userVerifier user.Verifier
|
||||
tlsCerts TLSCerts
|
||||
htmlHandlerRequestsDuration *prometheus.HistogramVec
|
||||
dsConfigHandlerRequestsDuration *prometheus.HistogramVec
|
||||
userService user.Service
|
||||
tempUserService tempUser.Service
|
||||
loginAttemptService loginAttempt.Service
|
||||
orgService org.Service
|
||||
orgDeletionService org.DeletionService
|
||||
TeamService team.Service
|
||||
accesscontrolService accesscontrol.Service
|
||||
annotationsRepo annotations.Repository
|
||||
tagService tag.Service
|
||||
oauthTokenService oauthtoken.OAuthTokenService
|
||||
statsService stats.Service
|
||||
authnService authn.Service
|
||||
starApi *starApi.API
|
||||
promRegister prometheus.Registerer
|
||||
promGatherer prometheus.Gatherer
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
namespacer request.NamespaceMapper
|
||||
anonService anonymous.Service
|
||||
userVerifier user.Verifier
|
||||
tlsCerts TLSCerts
|
||||
htmlHandlerRequestsDuration *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
type TLSCerts struct {
|
||||
@@ -383,15 +382,9 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
Name: "html_handler_requests_duration_seconds",
|
||||
Help: "Duration of requests handled by the index.go HTML handler",
|
||||
}, []string{"handler"}),
|
||||
dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "grafana",
|
||||
Name: "ds_config_handler_requests_duration_seconds",
|
||||
Help: "Duration of requests handled by datasource configuration handlers",
|
||||
}, []string{"code_path", "handler"}),
|
||||
}
|
||||
|
||||
promRegister.MustRegister(hs.htmlHandlerRequestsDuration)
|
||||
promRegister.MustRegister(hs.dsConfigHandlerRequestsDuration)
|
||||
|
||||
if hs.Listener != nil {
|
||||
hs.log.Debug("Using provided listener")
|
||||
|
||||
@@ -160,6 +160,7 @@ var serviceIdentityTokenPermissions = []string{
|
||||
"iam.grafana.app:*",
|
||||
"preferences.grafana.app:*", // user, team, and org preferences
|
||||
"collections.grafana.app:*", // user stars
|
||||
"apiextensions.grafana.app:*",
|
||||
|
||||
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
|
||||
"secret.grafana.app/securevalues:decrypt",
|
||||
|
||||
@@ -131,19 +131,31 @@ func NamespaceKeyFunc(gr schema.GroupResource) func(ctx context.Context, name st
|
||||
}
|
||||
}
|
||||
|
||||
// NoNamespaceKeyFunc is the default function for constructing storage paths
|
||||
// to a resource relative to the given prefix without a namespace.
|
||||
func NoNamespaceKeyFunc(ctx context.Context, prefix string, gr schema.GroupResource, name string) (string, error) {
|
||||
if len(name) == 0 {
|
||||
return "", apierrors.NewBadRequest("Name parameter required.")
|
||||
// ClusterScopedKeyFunc constructs storage paths for cluster-scoped resources (no namespace).
|
||||
func ClusterScopedKeyFunc(gr schema.GroupResource) func(ctx context.Context, name string) (string, error) {
|
||||
return func(ctx context.Context, name string) (string, error) {
|
||||
if len(name) == 0 {
|
||||
return "", apierrors.NewBadRequest("Name parameter required.")
|
||||
}
|
||||
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
|
||||
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
|
||||
}
|
||||
key := &Key{
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
Name: name,
|
||||
}
|
||||
return key.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// ClusterScopedKeyRootFunc is used by the generic registry store for cluster-scoped resources.
|
||||
func ClusterScopedKeyRootFunc(gr schema.GroupResource) func(ctx context.Context) string {
|
||||
return func(ctx context.Context) string {
|
||||
key := &Key{
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
}
|
||||
return key.String()
|
||||
}
|
||||
if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 {
|
||||
return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";")))
|
||||
}
|
||||
key := &Key{
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
Name: name,
|
||||
}
|
||||
return prefix + key.String(), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
@@ -12,16 +14,27 @@ func NewRegistryStore(scheme *runtime.Scheme, resourceInfo utils.ResourceInfo, o
|
||||
gv := resourceInfo.GroupVersion()
|
||||
gv.Version = runtime.APIVersionInternal
|
||||
strategy := NewStrategy(scheme, gv)
|
||||
|
||||
gr := resourceInfo.GroupResource()
|
||||
var keyRootFunc func(ctx context.Context) string
|
||||
var keyFunc func(ctx context.Context, name string) (string, error)
|
||||
|
||||
if resourceInfo.IsClusterScoped() {
|
||||
strategy = strategy.WithClusterScope()
|
||||
keyRootFunc = ClusterScopedKeyRootFunc(gr)
|
||||
keyFunc = ClusterScopedKeyFunc(gr)
|
||||
} else {
|
||||
keyRootFunc = KeyRootFunc(gr)
|
||||
keyFunc = NamespaceKeyFunc(gr)
|
||||
}
|
||||
|
||||
store := ®istry.Store{
|
||||
NewFunc: resourceInfo.NewFunc,
|
||||
NewListFunc: resourceInfo.NewListFunc,
|
||||
KeyRootFunc: KeyRootFunc(resourceInfo.GroupResource()),
|
||||
KeyFunc: NamespaceKeyFunc(resourceInfo.GroupResource()),
|
||||
KeyRootFunc: keyRootFunc,
|
||||
KeyFunc: keyFunc,
|
||||
PredicateFunc: Matcher,
|
||||
DefaultQualifiedResource: resourceInfo.GroupResource(),
|
||||
DefaultQualifiedResource: gr,
|
||||
SingularQualifiedResource: resourceInfo.SingularGroupResource(),
|
||||
TableConvertor: resourceInfo.TableConverter(),
|
||||
CreateStrategy: strategy,
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
_ "github.com/blugelabs/bluge"
|
||||
_ "github.com/blugelabs/bluge_segment_api"
|
||||
_ "github.com/crewjam/saml"
|
||||
_ "github.com/docker/go-connections/nat"
|
||||
_ "github.com/go-jose/go-jose/v4"
|
||||
_ "github.com/gobwas/glob"
|
||||
_ "github.com/googleapis/gax-go/v2"
|
||||
@@ -31,7 +30,6 @@ import (
|
||||
_ "github.com/spf13/cobra" // used by the standalone apiserver cli
|
||||
_ "github.com/spyzhov/ajson"
|
||||
_ "github.com/stretchr/testify/require"
|
||||
_ "github.com/testcontainers/testcontainers-go"
|
||||
_ "gocloud.dev/secrets/awskms"
|
||||
_ "gocloud.dev/secrets/azurekeyvault"
|
||||
_ "gocloud.dev/secrets/gcpkms"
|
||||
@@ -56,7 +54,9 @@ import (
|
||||
_ "github.com/grafana/e2e"
|
||||
_ "github.com/grafana/gofpdf"
|
||||
_ "github.com/grafana/gomemcache/memcache"
|
||||
_ "github.com/grafana/tempo/pkg/traceql"
|
||||
|
||||
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
|
||||
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
|
||||
_ "github.com/grafana/tempo/pkg/traceql"
|
||||
_ "github.com/testcontainers/testcontainers-go"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
package apiextensions
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
|
||||
apiextensionsopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
// CRDStorageProvider is an interface for creating CRD REST options getters.
|
||||
// Enterprise provides the real implementation, OSS returns nil.
|
||||
type CRDStorageProvider interface {
|
||||
NewCRDRESTOptionsGetter(
|
||||
delegate *apistore.RESTOptionsGetter,
|
||||
unifiedClient resource.ResourceClient,
|
||||
) genericregistry.RESTOptionsGetter
|
||||
}
|
||||
|
||||
// OSSCRDStorageProvider is the OSS implementation that returns nil (feature disabled)
|
||||
type OSSCRDStorageProvider struct{}
|
||||
|
||||
func ProvideOSSCRDStorageProvider() CRDStorageProvider {
|
||||
return &OSSCRDStorageProvider{}
|
||||
}
|
||||
|
||||
func (p *OSSCRDStorageProvider) NewCRDRESTOptionsGetter(
|
||||
delegate *apistore.RESTOptionsGetter,
|
||||
unifiedClient resource.ResourceClient,
|
||||
) genericregistry.RESTOptionsGetter {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ builder.APIGroupBuilder = (*Builder)(nil)
|
||||
|
||||
// Builder implements builder.APIGroupBuilder for CustomResourceDefinitions.
|
||||
// This implementation uses the Kubernetes apiextensions-apiserver for CRD handling,
|
||||
// adapted to work with Grafana's unified storage backend.
|
||||
//
|
||||
// IMPORTANT: This builder only registers the CRD types with the scheme.
|
||||
// The actual CRD storage and custom resource handling is done by the
|
||||
// Kubernetes apiextensions-apiserver, which is created separately and
|
||||
// chained as a delegate server.
|
||||
type Builder struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
accessClient authlib.AccessClient
|
||||
unifiedClient resource.ResourceClient
|
||||
apiExtensionsServer *apiextensionsapiserver.CustomResourceDefinitions
|
||||
storageProvider CRDStorageProvider
|
||||
}
|
||||
|
||||
// RegisterAPIService registers the apiextensions API group in single-tenant mode
|
||||
func RegisterAPIService(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
apiregistration builder.APIRegistrar,
|
||||
accessClient authlib.AccessClient,
|
||||
registerer prometheus.Registerer,
|
||||
unified resource.ResourceClient,
|
||||
storageProvider CRDStorageProvider,
|
||||
) (*Builder, error) {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b := &Builder{
|
||||
features: features,
|
||||
accessClient: accessClient,
|
||||
unifiedClient: unified,
|
||||
storageProvider: storageProvider,
|
||||
}
|
||||
|
||||
// Register the builder to install the schema
|
||||
apiregistration.RegisterAPI(b)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// GetAuthorizer returns the authorizer for CRD resources
|
||||
// Breaks locally now for ST, will need to test in MT
|
||||
// For ST just comment this out to test
|
||||
// func (b *Builder) GetAuthorizer() authorizer.Authorizer {
|
||||
// return grafanaauthorizer.NewServiceAuthorizer()
|
||||
// }
|
||||
|
||||
// NewAPIService creates an Builder for multi-tenant mode
|
||||
func NewAPIService(
|
||||
accessClient authlib.AccessClient,
|
||||
unified resource.ResourceClient,
|
||||
registerer prometheus.Registerer,
|
||||
features featuremgmt.FeatureToggles,
|
||||
storageProvider CRDStorageProvider,
|
||||
) (*Builder, error) {
|
||||
return &Builder{
|
||||
features: features,
|
||||
accessClient: accessClient,
|
||||
unifiedClient: unified,
|
||||
storageProvider: storageProvider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetGroupVersion returns the API group version for apiextensions.k8s.io/v1
|
||||
func (b *Builder) GetGroupVersion() schema.GroupVersion {
|
||||
return apiextensionsv1.SchemeGroupVersion
|
||||
}
|
||||
|
||||
// InstallSchema installs the CRD types into the scheme
|
||||
func (b *Builder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
gv := b.GetGroupVersion()
|
||||
|
||||
// Register the apiextensions types from the K8s apiextensions-apiserver
|
||||
// This uses the types and scheme from the K8s package
|
||||
metav1.AddToGroupVersion(scheme, gv)
|
||||
|
||||
// Add the CRD types to the scheme
|
||||
scheme.AddKnownTypes(gv,
|
||||
&apiextensionsv1.CustomResourceDefinition{},
|
||||
&apiextensionsv1.CustomResourceDefinitionList{},
|
||||
)
|
||||
|
||||
return scheme.SetVersionPriority(gv)
|
||||
}
|
||||
|
||||
func (b *Builder) AllowedV0Alpha1Resources() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAPIGroupInfo is a no-op for the apiextensions builder.
|
||||
// The actual CRD storage is created by the apiextensions server, not the builder.
|
||||
// This is called by the builder framework but we don't need to do anything here
|
||||
// since we're using the K8s apiextensions-apiserver which creates its own storage.
|
||||
func (b *Builder) UpdateAPIGroupInfo(
|
||||
_ *genericapiserver.APIGroupInfo,
|
||||
_ builder.APIGroupOptions,
|
||||
) error {
|
||||
// Don't install any storage here - the apiextensions server handles this
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOpenAPIDefinitions returns the OpenAPI definitions for CRD types
|
||||
func (b *Builder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return apiextensionsopenapi.GetOpenAPIDefinitions(ref)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAPIExtensionsServer creates the Kubernetes apiextensions-apiserver
|
||||
// This server handles CRD storage and custom resource (CR) handling.
|
||||
// It should be used as a delegate for the main Grafana API server.
|
||||
func (b *Builder) CreateAPIExtensionsServer(
|
||||
serverConfig genericapiserver.RecommendedConfig,
|
||||
delegationTarget genericapiserver.DelegationTarget,
|
||||
restOptsGetter *apistore.RESTOptionsGetter,
|
||||
) (*apiextensionsapiserver.CustomResourceDefinitions, error) {
|
||||
if restOptsGetter == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create the CRD REST options getter that uses unified storage
|
||||
crdRestOptsGetter := b.storageProvider.NewCRDRESTOptionsGetter(restOptsGetter, b.unifiedClient)
|
||||
if crdRestOptsGetter == nil {
|
||||
// Enterprise feature not available
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create a fresh copy of the config for the apiextensions server
|
||||
// We need to clear PostStartHooks to avoid conflicts with hooks
|
||||
// already registered by the main server (e.g., "playlist")
|
||||
apiExtensionsGenericConfig := serverConfig
|
||||
apiExtensionsGenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{}
|
||||
|
||||
// Set the RESTOptionsGetter on the GenericConfig
|
||||
// The K8s apiextensions-apiserver uses GenericConfig.RESTOptionsGetter for CRD storage
|
||||
// and ExtraConfig.CRDRESTOptionsGetter for Custom Resource storage
|
||||
apiExtensionsGenericConfig.RESTOptionsGetter = crdRestOptsGetter
|
||||
|
||||
// Enable the CRD resources in the API resource config
|
||||
apiResourceConfig := serverstorage.NewResourceConfig()
|
||||
apiResourceConfig.EnableVersions(apiextensionsv1.SchemeGroupVersion)
|
||||
apiExtensionsGenericConfig.MergedResourceConfig = apiResourceConfig
|
||||
|
||||
// Configure the apiextensions server
|
||||
apiextensionsConfig := &apiextensionsapiserver.Config{
|
||||
GenericConfig: &apiExtensionsGenericConfig,
|
||||
ExtraConfig: apiextensionsapiserver.ExtraConfig{
|
||||
// CRDRESTOptionsGetter is used for Custom Resource (CR) storage, not CRD storage
|
||||
CRDRESTOptionsGetter: crdRestOptsGetter,
|
||||
MasterCount: 1,
|
||||
// Webhook conversion is not supported yet
|
||||
ServiceResolver: nil,
|
||||
AuthResolverWrapper: nil,
|
||||
},
|
||||
}
|
||||
|
||||
server, err := apiextensionsConfig.Complete().New(delegationTarget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.apiExtensionsServer = server
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// GetAPIExtensionsServer returns the apiextensions server (if created)
|
||||
func (b *Builder) GetAPIExtensionsServer() *apiextensionsapiserver.CustomResourceDefinitions {
|
||||
return b.apiExtensionsServer
|
||||
}
|
||||
|
||||
// SetAPIServer is a no-op for compatibility with the builder interface
|
||||
func (b *Builder) SetAPIServer(server *genericapiserver.GenericAPIServer) {
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package apiregistry
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections"
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
@@ -20,6 +21,7 @@ type Service struct{}
|
||||
// ProvideRegistryServiceSink is an entry point for each service that will force initialization
|
||||
// and give each builder the chance to register itself with the main server
|
||||
func ProvideRegistryServiceSink(
|
||||
_ *apiextensions.Builder,
|
||||
_ *dashboardinternal.DashboardsAPIBuilder,
|
||||
_ *dashboardsnapshot.SnapshotsAPIBuilder,
|
||||
_ *datasource.DataSourceAPIBuilder,
|
||||
|
||||
@@ -3,9 +3,7 @@ package datasource
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -13,7 +11,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,9 +26,8 @@ var (
|
||||
)
|
||||
|
||||
type legacyStorage struct {
|
||||
datasources PluginDatasourceProvider
|
||||
resourceInfo *utils.ResourceInfo
|
||||
dsConfigHandlerRequestsDuration *prometheus.HistogramVec
|
||||
datasources PluginDatasourceProvider
|
||||
resourceInfo *utils.ResourceInfo
|
||||
}
|
||||
|
||||
func (s *legacyStorage) New() runtime.Object {
|
||||
@@ -61,21 +57,11 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
return s.datasources.GetDataSource(ctx, name)
|
||||
}
|
||||
|
||||
// Create implements rest.Creater.
|
||||
func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
ds, ok := obj.(*v0alpha1.DataSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected a datasource object")
|
||||
@@ -85,11 +71,6 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa
|
||||
|
||||
// Update implements rest.Updater.
|
||||
func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
old, err := s.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -126,11 +107,6 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
|
||||
|
||||
// Delete implements rest.GracefulDeleter.
|
||||
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
err := s.datasources.DeleteDataSource(ctx, name)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/promlib/models"
|
||||
@@ -219,11 +218,6 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
||||
legacyStore := &legacyStorage{
|
||||
datasources: b.datasources,
|
||||
resourceInfo: &ds,
|
||||
dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "grafana",
|
||||
Name: "ds_config_handler_requests_duration_seconds",
|
||||
Help: "Duration of requests handled by datasource configuration handlers",
|
||||
}, []string{"code_path", "handler"}),
|
||||
}
|
||||
unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter)
|
||||
if err != nil {
|
||||
|
||||
@@ -64,10 +64,6 @@ func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile stri
|
||||
}
|
||||
|
||||
func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg) (*APIBuilder, error) {
|
||||
if !cfg.OpenFeature.APIEnabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var staticEvaluator featuremgmt.StaticFlagEvaluator // No static evaluator needed for non-static provider
|
||||
var err error
|
||||
if cfg.OpenFeature.ProviderType == setting.StaticProviderType {
|
||||
|
||||
@@ -3,6 +3,7 @@ package apiregistry
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections"
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
@@ -59,6 +60,7 @@ var WireSet = wire.NewSet(
|
||||
provisioningExtras,
|
||||
|
||||
// Each must be added here *and* in the ServiceSink above
|
||||
apiextensions.RegisterAPIService,
|
||||
dashboardinternal.RegisterAPIService,
|
||||
dashboardsnapshot.RegisterAPIService,
|
||||
datasource.RegisterAPIService,
|
||||
|
||||
@@ -24,8 +24,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -44,8 +42,6 @@ type AppInstaller struct {
|
||||
func RegisterAppInstaller(
|
||||
cfgProvider configprovider.ConfigProvider,
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
pluginStore pluginstore.Store,
|
||||
pluginAssetsService *pluginassets.Service,
|
||||
) (*AppInstaller, error) {
|
||||
grafanaComAPIURL := os.Getenv("GRAFANA_COM_API_URL")
|
||||
if grafanaComAPIURL == "" {
|
||||
@@ -54,8 +50,7 @@ func RegisterAppInstaller(
|
||||
|
||||
coreProvider := meta.NewCoreProvider()
|
||||
cloudProvider := meta.NewCloudProvider(grafanaComAPIURL)
|
||||
localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService)
|
||||
metaProviderManager := meta.NewProviderManager(localProvider, coreProvider, cloudProvider)
|
||||
metaProviderManager := meta.NewProviderManager(coreProvider, cloudProvider)
|
||||
specificConfig := &pluginsapp.PluginAppConfig{
|
||||
MetaProviderManager: metaProviderManager,
|
||||
}
|
||||
|
||||
Generated
+17
-6
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/registry/apis"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
@@ -668,7 +669,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -788,7 +789,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appInstaller, err := plugins.RegisterAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService)
|
||||
appInstaller, err := plugins.RegisterAppInstaller(configProvider, eventualRestConfigProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -861,6 +862,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService)
|
||||
ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService)
|
||||
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
crdStorageProvider := apiextensions.ProvideOSSCRDStorageProvider()
|
||||
apiextensionsBuilder, err := apiextensions.RegisterAPIService(cfg, featureToggles, apiserverService, accessClient, registerer, resourceClient, crdStorageProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService)
|
||||
@@ -914,7 +920,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(apiextensionsBuilder, dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1312,7 +1318,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg)
|
||||
promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService)
|
||||
provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1438,7 +1444,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appInstaller, err := plugins.RegisterAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService)
|
||||
appInstaller, err := plugins.RegisterAppInstaller(configProvider, eventualRestConfigProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1511,6 +1517,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService)
|
||||
ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService)
|
||||
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
crdStorageProvider := apiextensions.ProvideOSSCRDStorageProvider()
|
||||
apiextensionsBuilder, err := apiextensions.RegisterAPIService(cfg, featureToggles, apiserverService, accessClient, registerer, resourceClient, crdStorageProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService)
|
||||
@@ -1564,7 +1575,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(apiextensionsBuilder, dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
apisregistry "github.com/grafana/grafana/pkg/registry/apis"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/apiextensions"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/extras"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
@@ -149,6 +150,7 @@ var wireExtsBasicSet = wire.NewSet(
|
||||
sql.ProvideStorageBackend,
|
||||
builder.ProvideDefaultBuildHandlerChainFuncFromBuilders,
|
||||
aggregatorrunner.ProvideNoopAggregatorConfigurator,
|
||||
apiextensions.ProvideOSSCRDStorageProvider,
|
||||
apisregistry.WireSetExts,
|
||||
gsmKMSProviders.ProvideOSSKMSProviders,
|
||||
secret.ProvideSecureValueClient,
|
||||
|
||||
@@ -3,6 +3,7 @@ package aggregatorrunner
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
|
||||
@@ -21,6 +22,10 @@ func (n NoopAggregatorConfigurator) Run(ctx context.Context, transport *options.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n *NoopAggregatorConfigurator) SetCRDInformer(_ apiextensionsinformers.CustomResourceDefinitionInformer) {
|
||||
// noop
|
||||
}
|
||||
|
||||
func ProvideNoopAggregatorConfigurator() AggregatorRunner {
|
||||
return &NoopAggregatorConfigurator{}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package aggregatorrunner
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
|
||||
@@ -21,4 +22,8 @@ type AggregatorRunner interface {
|
||||
|
||||
// Run starts the complete apiserver chain, expects it executes any logic inside a goroutine and doesn't block. Returns the running server.
|
||||
Run(ctx context.Context, transport *options.RoundTripperFunc, stoppedCh chan error) (*genericapiserver.GenericAPIServer, error)
|
||||
|
||||
// SetCRDInformer sets the CRD informer for auto-registering APIServices for CRDs.
|
||||
// This should be called before Configure if CRD API is enabled.
|
||||
SetCRDInformer(informer apiextensionsinformers.CustomResourceDefinitionInformer)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
k8srequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
"k8s.io/apiserver/pkg/util/openapi"
|
||||
k8sscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
k8stracing "k8s.io/component-base/tracing"
|
||||
@@ -72,13 +73,36 @@ var PathRewriters = []filters.PathRewriter{
|
||||
},
|
||||
}
|
||||
|
||||
// GetDefaultBuildHandlerChainFuncForAggregator is a replica of GetDefaultBuildHandlerChainFunc except it skips custom routes handling
|
||||
func GetDefaultBuildHandlerChainFuncForAggregator() BuildHandlerChainFunc {
|
||||
return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler {
|
||||
// filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context
|
||||
handler := filters.WithRequester(delegateHandler)
|
||||
|
||||
// Call DefaultBuildHandlerChain on the main entrypoint http.Handler
|
||||
// See https://github.com/kubernetes/apiserver/blob/v0.28.0/pkg/server/config.go#L906
|
||||
// DefaultBuildHandlerChain provides many things, notably CORS, HSTS, cache-control, authz and latency tracking
|
||||
handler = genericapiserver.DefaultBuildHandlerChain(handler, c)
|
||||
|
||||
handler = filters.WithAcceptHeader(handler)
|
||||
handler = filters.WithPathRewriters(handler, PathRewriters)
|
||||
handler = k8stracing.WithTracing(handler, c.TracerProvider, "KubernetesAPI")
|
||||
handler = filters.WithExtractJaegerTrace(handler)
|
||||
// Configure filters.WithPanicRecovery to not crash on panic
|
||||
utilruntime.ReallyCrash = false
|
||||
|
||||
return handler
|
||||
}
|
||||
}
|
||||
func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc {
|
||||
return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler {
|
||||
requestHandler, err := GetCustomRoutesHandler(
|
||||
delegateHandler,
|
||||
c.LoopbackClientConfig,
|
||||
builders,
|
||||
reg)
|
||||
reg,
|
||||
c.MergedResourceConfig,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error()))
|
||||
}
|
||||
@@ -105,6 +129,8 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.
|
||||
}
|
||||
}
|
||||
|
||||
// SetupConfig sets up the server config for the API server
|
||||
// specify isAggregator=true, if the chain is being constructed for kube-aggregator
|
||||
func SetupConfig(
|
||||
scheme *runtime.Scheme,
|
||||
serverConfig *genericapiserver.RecommendedConfig,
|
||||
@@ -114,6 +140,7 @@ func SetupConfig(
|
||||
gvs []schema.GroupVersion,
|
||||
additionalOpenAPIDefGetters []common.GetOpenAPIDefinitions,
|
||||
reg prometheus.Registerer,
|
||||
apiResourceConfig *serverstorage.ResourceConfig,
|
||||
) error {
|
||||
serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders)
|
||||
defsGetter := GetOpenAPIDefinitions(builders, additionalOpenAPIDefGetters...)
|
||||
@@ -126,7 +153,7 @@ func SetupConfig(
|
||||
openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme))
|
||||
|
||||
// Add the custom routes to service discovery
|
||||
serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs)
|
||||
serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs, apiResourceConfig)
|
||||
serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) {
|
||||
meta := r.Metadata()
|
||||
kind := ""
|
||||
@@ -287,6 +314,7 @@ func InstallAPIs(
|
||||
features featuremgmt.FeatureToggles,
|
||||
dualWriterMetrics *grafanarest.DualWriterMetrics,
|
||||
builderMetrics *BuilderMetrics,
|
||||
apiResourceConfig *serverstorage.ResourceConfig,
|
||||
) error {
|
||||
// dual writing is only enabled when the storage type is not legacy.
|
||||
// this is needed to support setting a default RESTOptionsGetter for new APIs that don't
|
||||
@@ -401,34 +429,9 @@ func InstallAPIs(
|
||||
for group, buildersForGroup := range buildersGroupMap {
|
||||
g := genericapiserver.NewDefaultAPIGroupInfo(group, scheme, metav1.ParameterCodec, codecs)
|
||||
for _, b := range buildersForGroup {
|
||||
if err := b.UpdateAPIGroupInfo(&g, APIGroupOptions{
|
||||
Scheme: scheme,
|
||||
OptsGetter: optsGetter,
|
||||
DualWriteBuilder: dualWrite,
|
||||
MetricsRegister: reg,
|
||||
StorageOptsRegister: optsregister,
|
||||
StorageOpts: storageOpts,
|
||||
}); err != nil {
|
||||
if err := installAPIGroupsForBuilder(&g, group, b, apiResourceConfig, scheme, optsGetter, dualWrite, reg, optsregister, storageOpts, features); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(g.PrioritizedVersions) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok {
|
||||
for name := range resources {
|
||||
if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) {
|
||||
delete(resources, name)
|
||||
}
|
||||
}
|
||||
if len(resources) == 0 {
|
||||
delete(g.VersionedResourcesStorageMap, "v0alpha1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skip installing the group if there are no resources left after filtering
|
||||
@@ -445,6 +448,53 @@ func InstallAPIs(
|
||||
return nil
|
||||
}
|
||||
|
||||
func installAPIGroupsForBuilder(g *genericapiserver.APIGroupInfo, group string, b APIGroupBuilder, apiResourceConfig *serverstorage.ResourceConfig, scheme *runtime.Scheme,
|
||||
optsGetter generic.RESTOptionsGetter, dualWrite grafanarest.DualWriteBuilder, reg prometheus.Registerer, optsregister apistore.StorageOptionsRegister,
|
||||
storageOpts *options.StorageOptions, features featuremgmt.FeatureToggles) error {
|
||||
if err := b.UpdateAPIGroupInfo(g, APIGroupOptions{
|
||||
Scheme: scheme,
|
||||
OptsGetter: optsGetter,
|
||||
DualWriteBuilder: dualWrite,
|
||||
MetricsRegister: reg,
|
||||
StorageOptsRegister: optsregister,
|
||||
StorageOpts: storageOpts,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(g.PrioritizedVersions) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// filter out api groups that are disabled in APIEnablementOptions
|
||||
for version := range g.VersionedResourcesStorageMap {
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: group,
|
||||
Version: version,
|
||||
}
|
||||
if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) {
|
||||
klog.InfoS("Skipping storage for disabled resource", "gvr", gvr.String())
|
||||
delete(g.VersionedResourcesStorageMap, version)
|
||||
}
|
||||
}
|
||||
|
||||
// if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok {
|
||||
for name := range resources {
|
||||
if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) {
|
||||
delete(resources, name)
|
||||
}
|
||||
}
|
||||
if len(resources) == 0 {
|
||||
delete(g.VersionedResourcesStorageMap, "v0alpha1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPostStartHooks adds post start hooks to a generic API server config
|
||||
func AddPostStartHooks(
|
||||
config *genericapiserver.RecommendedConfig,
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
"k8s.io/klog/v2"
|
||||
openapi "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
@@ -76,6 +78,7 @@ func addBuilderRoutes(
|
||||
targetGroupVersion schema.GroupVersion,
|
||||
openAPISpec *spec3.OpenAPI,
|
||||
apiGroupBuilders []APIGroupBuilder,
|
||||
apiResourceConfig *serverstorage.ResourceConfig,
|
||||
) (*spec3.OpenAPI, error) {
|
||||
for _, apiGroupBuilder := range apiGroupBuilders {
|
||||
// Optionally include raw http handlers for all builders
|
||||
@@ -107,12 +110,24 @@ func addBuilderRoutes(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter out api groups that are disabled in APIEnablementOptions
|
||||
for path := range openAPISpec.Paths.Paths {
|
||||
if strings.HasPrefix(path, "/apis/"+targetGroupVersion.String()+"/") {
|
||||
gv := targetGroupVersion.WithResource("")
|
||||
if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gv) {
|
||||
klog.InfoS("removing openapi routes for disabled resource", "gv", gv.String())
|
||||
delete(openAPISpec.Paths.Paths, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return openAPISpec, nil
|
||||
}
|
||||
|
||||
// Modify the OpenAPI spec to include the additional routes.
|
||||
// nolint:gocyclo
|
||||
func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion, apiResourceConfig *serverstorage.ResourceConfig) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
if s.Paths == nil {
|
||||
return s, nil
|
||||
@@ -227,7 +242,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s
|
||||
}
|
||||
}
|
||||
}
|
||||
return addBuilderRoutes(gv, ©, builders)
|
||||
return addBuilderRoutes(gv, ©, builders, apiResourceConfig)
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
klog "k8s.io/klog/v2"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
)
|
||||
|
||||
@@ -14,7 +16,7 @@ type requestHandler struct {
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer) (http.Handler, error) {
|
||||
func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) {
|
||||
useful := false // only true if any routes exist anywhere
|
||||
router := mux.NewRouter()
|
||||
|
||||
@@ -27,6 +29,12 @@ func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient
|
||||
}
|
||||
|
||||
for _, gv := range GetGroupVersions(builder) {
|
||||
// filter out api groups that are disabled in APIEnablementOptions
|
||||
gvr := gv.WithResource("")
|
||||
if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) {
|
||||
klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String())
|
||||
continue
|
||||
}
|
||||
routes := provider.GetAPIRoutes(gv)
|
||||
if routes == nil {
|
||||
continue
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/dskit/services"
|
||||
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
|
||||
@@ -316,7 +318,11 @@ func (s *service) start(ctx context.Context) error {
|
||||
s.cfg.BuildBranch,
|
||||
)
|
||||
|
||||
if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, appinstaller.NewAPIResourceConfig(s.appInstallers), s.scheme); err != nil {
|
||||
apiResourceConfig := appinstaller.NewAPIResourceConfig(s.appInstallers)
|
||||
// add the builder group versions to the api resource config
|
||||
apiResourceConfig.EnableVersions(groupVersions...)
|
||||
|
||||
if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, apiResourceConfig, s.scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -331,6 +337,7 @@ func (s *service) start(ctx context.Context) error {
|
||||
serverConfig.MaxRequestBodyBytes = MaxRequestBodyBytes
|
||||
|
||||
var optsregister apistore.StorageOptionsRegister
|
||||
var restOptsGetter *apistore.RESTOptionsGetter
|
||||
|
||||
if o.StorageOptions.StorageType == grafanaapiserveroptions.StorageTypeEtcd {
|
||||
if err := o.RecommendedOptions.Etcd.Validate(); len(err) > 0 {
|
||||
@@ -340,9 +347,9 @@ func (s *service) start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
getter := apistore.NewRESTOptionsGetterForClient(s.unified, s.secrets, o.RecommendedOptions.Etcd.StorageConfig, s.restConfigProvider)
|
||||
optsregister = getter.RegisterOptions
|
||||
serverConfig.RESTOptionsGetter = getter
|
||||
restOptsGetter = apistore.NewRESTOptionsGetterForClient(s.unified, s.secrets, o.RecommendedOptions.Etcd.StorageConfig, s.restConfigProvider)
|
||||
optsregister = restOptsGetter.RegisterOptions
|
||||
serverConfig.RESTOptionsGetter = restOptsGetter
|
||||
}
|
||||
|
||||
defGetters := []common.GetOpenAPIDefinitions{
|
||||
@@ -359,6 +366,7 @@ func (s *service) start(ctx context.Context) error {
|
||||
groupVersions,
|
||||
defGetters,
|
||||
s.metrics,
|
||||
apiResourceConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -378,8 +386,35 @@ func (s *service) start(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to register post start hooks for app installers: %w", err)
|
||||
}
|
||||
|
||||
// Create the server
|
||||
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
|
||||
// Determine the delegate for the main server
|
||||
var delegationTarget = genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler)
|
||||
var apiExtensionsServer *apiextensionsapiserver.CustomResourceDefinitions
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
apiExtensionsEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagApiExtensions)
|
||||
|
||||
if apiExtensionsEnabled && restOptsGetter != nil {
|
||||
// Create the K8s apiextensions-apiserver for CRD/CR handling
|
||||
// This server handles:
|
||||
// - CRD storage (create, get, list, update, delete CRDs)
|
||||
// - Custom resource handling (dynamically serves CRs based on registered CRDs)
|
||||
s.log.Info("Creating apiextensions server")
|
||||
apiExtensionsServer, err = s.createAPIExtensionsServer(builders, serverConfig, delegationTarget, restOptsGetter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create apiextensions server: %w", err)
|
||||
}
|
||||
if apiExtensionsServer != nil {
|
||||
s.log.Info("apiextensions server created successfully, chaining as delegate")
|
||||
// Chain the apiextensions server as the delegate
|
||||
// Requests go: grafana-apiserver -> apiextensions-apiserver -> notFoundHandler
|
||||
delegationTarget = apiExtensionsServer.GenericAPIServer
|
||||
} else {
|
||||
s.log.Warn("apiextensions server was not created (returned nil)")
|
||||
}
|
||||
}
|
||||
|
||||
// Create the main Grafana API server
|
||||
server, err := serverConfig.Complete().New("grafana-apiserver", delegationTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -400,6 +435,7 @@ func (s *service) start(ctx context.Context) error {
|
||||
s.features,
|
||||
s.dualWriterMetrics,
|
||||
s.builderMetrics,
|
||||
apiResourceConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -434,6 +470,11 @@ func (s *service) start(ctx context.Context) error {
|
||||
isDataplaneAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator)
|
||||
|
||||
if isKubernetesAggregatorEnabled {
|
||||
// Pass CRD informer to aggregator if apiextensions is enabled (for auto-registering APIServices for CRDs)
|
||||
if apiExtensionsServer != nil && apiExtensionsServer.Informers != nil {
|
||||
s.log.Info("Starting CRD informer for apiextensions service")
|
||||
s.aggregatorRunner.SetCRDInformer(apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions())
|
||||
}
|
||||
aggregatorServer, err := s.aggregatorRunner.Configure(s.options, serverConfig, delegate, s.scheme, builders)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -638,3 +679,30 @@ func useNamespaceFromPath(path string, user *user.SignedInUser) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createAPIExtensionsServer creates the Kubernetes apiextensions-apiserver for CRD/CR handling.
|
||||
// This server is chained as a delegate, handling CRD storage and dynamic custom resource serving.
|
||||
func (s *service) createAPIExtensionsServer(
|
||||
builders []builder.APIGroupBuilder,
|
||||
serverConfig *genericapiserver.RecommendedConfig,
|
||||
delegationTarget genericapiserver.DelegationTarget,
|
||||
restOptsGetter *apistore.RESTOptionsGetter,
|
||||
) (*apiextensionsapiserver.CustomResourceDefinitions, error) {
|
||||
// Find the apiextensions Builder
|
||||
type apiExtensionsCreator interface {
|
||||
CreateAPIExtensionsServer(
|
||||
serverConfig genericapiserver.RecommendedConfig,
|
||||
delegationTarget genericapiserver.DelegationTarget,
|
||||
restOptsGetter *apistore.RESTOptionsGetter,
|
||||
) (*apiextensionsapiserver.CustomResourceDefinitions, error)
|
||||
}
|
||||
|
||||
for _, b := range builders {
|
||||
if creator, ok := b.(apiExtensionsCreator); ok {
|
||||
return creator.CreateAPIExtensionsServer(*serverConfig, delegationTarget, restOptsGetter)
|
||||
}
|
||||
}
|
||||
|
||||
// No apiextensions builder found
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -755,6 +755,13 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "apiExtensions",
|
||||
Description: "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "groupByVariable",
|
||||
Description: "Enable groupBy variable support in scenes dashboards",
|
||||
@@ -1801,10 +1808,10 @@ var (
|
||||
{
|
||||
Name: "azureResourcePickerUpdates",
|
||||
Description: "Enables the updated Azure Monitor resource picker",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Stage: FeatureStagePublicPreview,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaPartnerPluginsSquad,
|
||||
Expression: "true",
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "prometheusTypeMigration",
|
||||
@@ -1895,10 +1902,10 @@ var (
|
||||
{
|
||||
Name: "newPanelPadding",
|
||||
Description: "Increases panel padding globally",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
FrontendOnly: true,
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
Expression: "true",
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "onlyStoreActionSets",
|
||||
|
||||
Generated
+3
-2
@@ -104,6 +104,7 @@ sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,fa
|
||||
sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true
|
||||
kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
kubernetesAggregatorCapTokenAuth,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
apiExtensions,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
groupByVariable,experimental,@grafana/dashboards-squad,false,false,false
|
||||
scopeFilters,experimental,@grafana/dashboards-squad,false,false,false
|
||||
oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false
|
||||
@@ -245,7 +246,7 @@ teamFolders,experimental,@grafana/grafana-search-navigate-organise,false,false,f
|
||||
interactiveLearning,preview,@grafana/pathfinder,false,false,false
|
||||
alertingTriage,experimental,@grafana/alerting-squad,false,false,false
|
||||
graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
azureResourcePickerUpdates,GA,@grafana/partner-datasources,false,false,true
|
||||
azureResourcePickerUpdates,preview,@grafana/partner-datasources,false,false,true
|
||||
prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false
|
||||
pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,false
|
||||
tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false
|
||||
@@ -257,7 +258,7 @@ newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true
|
||||
preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true
|
||||
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
|
||||
newPanelPadding,experimental,@grafana/dashboards-squad,false,false,false
|
||||
onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false
|
||||
panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false
|
||||
kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false
|
||||
|
||||
|
Generated
+8
@@ -311,6 +311,10 @@ const (
|
||||
// Enable CAP token based authentication in grafana's embedded kube-aggregator
|
||||
FlagKubernetesAggregatorCapTokenAuth = "kubernetesAggregatorCapTokenAuth"
|
||||
|
||||
// FlagApiExtensions
|
||||
// Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration
|
||||
FlagApiExtensions = "apiExtensions"
|
||||
|
||||
// FlagGroupByVariable
|
||||
// Enable groupBy variable support in scenes dashboards
|
||||
FlagGroupByVariable = "groupByVariable"
|
||||
@@ -742,6 +746,10 @@ const (
|
||||
// Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded
|
||||
FlagPluginStoreServiceLoading = "pluginStoreServiceLoading"
|
||||
|
||||
// FlagNewPanelPadding
|
||||
// Increases panel padding globally
|
||||
FlagNewPanelPadding = "newPanelPadding"
|
||||
|
||||
// FlagOnlyStoreActionSets
|
||||
// When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission
|
||||
FlagOnlyStoreActionSets = "onlyStoreActionSets"
|
||||
|
||||
+21
-15
@@ -633,6 +633,19 @@
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "apiExtensions",
|
||||
"resourceVersion": "1764159104213",
|
||||
"creationTimestamp": "2025-11-26T12:11:44Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable Kubernetes CustomResourceDefinition (CRD) support with dynamic API registration",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-app-platform-squad",
|
||||
"requiresRestart": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "appPlatformGrpcClientAuth",
|
||||
@@ -766,19 +779,16 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "azureResourcePickerUpdates",
|
||||
"resourceVersion": "1764153435365",
|
||||
"resourceVersion": "1763734583253",
|
||||
"creationTimestamp": "2025-07-31T22:56:50Z",
|
||||
"deletionTimestamp": "2025-08-01T11:30:17Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-11-26 10:37:15.365919 +0000 UTC"
|
||||
}
|
||||
"deletionTimestamp": "2025-08-01T11:30:17Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the updated Azure Monitor resource picker",
|
||||
"stage": "GA",
|
||||
"stage": "preview",
|
||||
"codeowner": "@grafana/partner-datasources",
|
||||
"frontend": true,
|
||||
"expression": "true"
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2362,18 +2372,14 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "newPanelPadding",
|
||||
"resourceVersion": "1764168915089",
|
||||
"creationTimestamp": "2025-11-12T15:40:46Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-11-26 14:55:15.089551 +0000 UTC"
|
||||
}
|
||||
"resourceVersion": "1763734583253",
|
||||
"creationTimestamp": "2025-11-12T15:40:46Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Increases panel padding globally",
|
||||
"stage": "preview",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/dashboards-squad",
|
||||
"frontend": true,
|
||||
"expression": "true"
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -165,21 +165,6 @@ func (aq *AlertQuery) setMaxDatapoints() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setRefID sets the model refId if it's missing or invalid
|
||||
func (aq *AlertQuery) setRefID() error {
|
||||
if aq.modelProps == nil {
|
||||
err := aq.setModelProps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if refID, ok := aq.modelProps["refId"].(string); !ok || refID != aq.RefID {
|
||||
aq.modelProps["refId"] = aq.RefID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (aq *AlertQuery) GetMaxDatapoints() (int64, error) {
|
||||
err := aq.setMaxDatapoints()
|
||||
if err != nil {
|
||||
@@ -271,11 +256,6 @@ func (aq *AlertQuery) GetModel() ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = aq.setRefID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = aq.setIntervalMS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -17,7 +17,6 @@ type Plugin struct {
|
||||
|
||||
// App fields
|
||||
Parent *ParentPlugin
|
||||
Children []string
|
||||
IncludedInAppID string
|
||||
DefaultNavURL string
|
||||
Pinned bool
|
||||
@@ -86,18 +85,6 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
|
||||
dto.Parent = &ParentPlugin{ID: p.Parent.ID}
|
||||
}
|
||||
|
||||
if len(p.Children) > 0 {
|
||||
children := make([]string, 0, len(p.Children))
|
||||
for _, child := range p.Children {
|
||||
if child != nil {
|
||||
children = append(children, child.ID)
|
||||
}
|
||||
}
|
||||
if len(children) > 0 {
|
||||
dto.Children = children
|
||||
}
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
@@ -10,12 +9,10 @@ import (
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
@@ -31,7 +28,7 @@ type DashboardProvisioner interface {
|
||||
}
|
||||
|
||||
// DashboardProvisionerFactory creates DashboardProvisioners based on input
|
||||
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (DashboardProvisioner, error)
|
||||
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (DashboardProvisioner, error)
|
||||
|
||||
// Provisioner is responsible for syncing dashboard from disk to Grafana's database.
|
||||
type Provisioner struct {
|
||||
@@ -41,8 +38,6 @@ type Provisioner struct {
|
||||
duplicateValidator duplicateValidator
|
||||
provisioner dashboards.DashboardProvisioningService
|
||||
dual dualwrite.Service
|
||||
serverLock *serverlock.ServerLockService
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func (provider *Provisioner) HasDashboardSources() bool {
|
||||
@@ -50,7 +45,7 @@ func (provider *Provisioner) HasDashboardSources() bool {
|
||||
}
|
||||
|
||||
// New returns a new DashboardProvisioner
|
||||
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, cfg *setting.Cfg, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service, serverLockService *serverlock.ServerLockService) (DashboardProvisioner, error) {
|
||||
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service) (DashboardProvisioner, error) {
|
||||
logger := log.New("provisioning.dashboard")
|
||||
cfgReader := &configReader{path: configDirectory, log: logger, orgExists: utils.NewOrgExistsChecker(orgService)}
|
||||
configs, err := cfgReader.readConfig(ctx)
|
||||
@@ -83,8 +78,6 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
|
||||
duplicateValidator: newDuplicateValidator(logger, fileReaders),
|
||||
provisioner: provisioner,
|
||||
dual: dual,
|
||||
serverLock: serverLockService,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
return d, nil
|
||||
@@ -102,53 +95,23 @@ func (provider *Provisioner) Provision(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
var errProvisioning error
|
||||
provider.log.Info("starting to provision dashboards")
|
||||
|
||||
// retry obtaining the lock for 20 attempts
|
||||
retryOpt := func(attempts int) error {
|
||||
if attempts < 20 {
|
||||
return nil
|
||||
}
|
||||
return errors.New("retries exhausted")
|
||||
}
|
||||
|
||||
lockTimeConfig := serverlock.LockTimeConfig{
|
||||
// if a replica crashes while holding the lock, other replicas can obtain the
|
||||
// lock after this duration (15s default value, might be configured via config file)
|
||||
MaxInterval: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds) * time.Second,
|
||||
|
||||
// wait beetween 100ms and 1s before retrying to obtain the lock (default values, might be configured via config file)
|
||||
MinWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMinWaitMs) * time.Millisecond,
|
||||
MaxWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs) * time.Millisecond,
|
||||
}
|
||||
|
||||
// this means that if we fail to obtain the lock after ~10 seconds, we return an error
|
||||
lockErr := provider.serverLock.LockExecuteAndReleaseWithRetries(ctx, "provisioning_dashboards", lockTimeConfig, func(ctx context.Context) {
|
||||
provider.log.Info("starting to provision dashboards")
|
||||
|
||||
for _, reader := range provider.fileReaders {
|
||||
if err := reader.walkDisk(ctx); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// don't stop the provisioning service in case the folder is missing. The folder can appear after the startup
|
||||
provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
errProvisioning = fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err)
|
||||
return
|
||||
for _, reader := range provider.fileReaders {
|
||||
if err := reader.walkDisk(ctx); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// don't stop the provisioning service in case the folder is missing. The folder can appear after the startup
|
||||
provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err)
|
||||
}
|
||||
|
||||
provider.duplicateValidator.validate()
|
||||
provider.log.Info("finished to provision dashboards")
|
||||
}, retryOpt)
|
||||
|
||||
if lockErr != nil {
|
||||
provider.log.Error("Failed to obtain dashboard provisioning lock", "error", lockErr)
|
||||
return lockErr
|
||||
}
|
||||
|
||||
return errProvisioning
|
||||
provider.duplicateValidator.validate()
|
||||
provider.log.Info("finished to provision dashboards")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader.
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/grafana/dskit/services"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -65,7 +64,6 @@ func ProvideService(
|
||||
tracer tracing.Tracer,
|
||||
dual dualwrite.Service,
|
||||
promTypeMigrationProvider promtypemigration.PromTypeMigrationProvider,
|
||||
serverLockService *serverlock.ServerLockService,
|
||||
) (*ProvisioningServiceImpl, error) {
|
||||
s := &ProvisioningServiceImpl{
|
||||
Cfg: cfg,
|
||||
@@ -94,7 +92,6 @@ func ProvideService(
|
||||
tracer: tracer,
|
||||
migratePrometheusType: promTypeMigrationProvider.Run,
|
||||
dual: dual,
|
||||
serverLock: serverLockService,
|
||||
}
|
||||
|
||||
s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName)
|
||||
@@ -169,7 +166,7 @@ func (ps *ProvisioningServiceImpl) running(ctx context.Context) error {
|
||||
|
||||
func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error {
|
||||
dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards")
|
||||
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.Cfg, ps.orgService, ps.dashboardService, ps.folderService, ps.dual, ps.serverLock)
|
||||
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %w", "Failed to create provisioner", err)
|
||||
}
|
||||
@@ -245,7 +242,6 @@ type ProvisioningServiceImpl struct {
|
||||
resourcePermissions accesscontrol.ReceiverPermissionsService
|
||||
tracer tracing.Tracer
|
||||
dual dualwrite.Service
|
||||
serverLock *serverlock.ServerLockService
|
||||
migratePrometheusType func(context.Context) error
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -21,7 +20,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
@@ -162,7 +160,7 @@ func setup(t *testing.T) *serviceTestStruct {
|
||||
searchStub := searchV2.NewStubSearchService()
|
||||
|
||||
service, err := newProvisioningServiceImpl(
|
||||
func(context.Context, string, dashboardstore.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (dashboards.DashboardProvisioner, error) {
|
||||
func(context.Context, string, dashboardstore.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (dashboards.DashboardProvisioner, error) {
|
||||
serviceTest.dashboardProvisionerInstantiations++
|
||||
return serviceTest.mock, nil
|
||||
},
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"gopkg.in/ini.v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/transport"
|
||||
|
||||
authlib "github.com/grafana/authlib/authn"
|
||||
logging "github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/semconv"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/setting")
|
||||
|
||||
const LogPrefix = "setting.service"
|
||||
|
||||
const DefaultPageSize = int64(500)
|
||||
const DefaultQPS = float32(10)
|
||||
const DefaultBurst = 25
|
||||
|
||||
const (
|
||||
ApiGroup = "setting.grafana.app"
|
||||
apiVersion = "v0alpha1"
|
||||
resource = "settings"
|
||||
kind = "Setting"
|
||||
listKind = "SettingList"
|
||||
)
|
||||
|
||||
var settingGroupVersion = schema.GroupVersionResource{
|
||||
Group: ApiGroup,
|
||||
Version: apiVersion,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
var settingGroupListKind = map[schema.GroupVersionResource]string{
|
||||
settingGroupVersion: listKind,
|
||||
}
|
||||
|
||||
type remoteSettingServiceMetrics struct {
|
||||
listDuration *prometheus.HistogramVec
|
||||
listResultSize *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
// Service retrieves configuration settings from a remote settings service.
|
||||
//
|
||||
// The service uses label selectors to filter settings. Settings are labeled with
|
||||
// "section" and "key" labels matching their spec fields.
|
||||
//
|
||||
// Example - Select all settings:
|
||||
//
|
||||
// ctx := request.WithNamespace(context.Background(), "my-namespace")
|
||||
// ini, err := service.ListAsIni(ctx, metav1.LabelSelector{})
|
||||
//
|
||||
// Example - Select settings from specific sections:
|
||||
//
|
||||
// selector := metav1.LabelSelector{
|
||||
// MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
// {
|
||||
// Key: "section",
|
||||
// Operator: metav1.LabelSelectorOpIn,
|
||||
// Values: []string{"database", "server"},
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// ini, err := service.ListAsIni(ctx, selector)
|
||||
//
|
||||
// Example - Select settings from a single section with specific labels:
|
||||
//
|
||||
// selector := metav1.LabelSelector{
|
||||
// MatchLabels: map[string]string{
|
||||
// "section": "database",
|
||||
// },
|
||||
// }
|
||||
// settings, err := service.List(ctx, selector)
|
||||
type Service interface {
|
||||
prometheus.Collector
|
||||
// ListAsIni retrieves settings filtered by a label selector from the namespace in context
|
||||
// and returns them as an ini.File.
|
||||
//
|
||||
// The namespace must be present in the context, ie: via request.WithNamespace.
|
||||
// An empty selector returns all settings in the namespace.
|
||||
ListAsIni(ctx context.Context, selector metav1.LabelSelector) (*ini.File, error)
|
||||
|
||||
// List retrieves settings filtered by a label selector from the namespace in context
|
||||
// and returns them as a slice of Setting structs.
|
||||
//
|
||||
// The namespace must be present in the context, ie: via request.WithNamespace.
|
||||
// An empty selector returns all settings in the namespace.
|
||||
List(ctx context.Context, selector metav1.LabelSelector) ([]*Setting, error)
|
||||
}
|
||||
|
||||
type remoteSettingService struct {
|
||||
dynamicClient dynamic.Interface
|
||||
log logging.Logger
|
||||
pageSize int64
|
||||
metrics remoteSettingServiceMetrics
|
||||
}
|
||||
|
||||
var _ Service = (*remoteSettingService)(nil)
|
||||
var _ prometheus.Collector = (*remoteSettingService)(nil)
|
||||
|
||||
// Config configures a Service.
|
||||
type Config struct {
|
||||
// URL is the base URL for the remote settings service (required).
|
||||
URL string
|
||||
// TokenExchangeClient authenticates requests (required if WrapTransport is not set).
|
||||
TokenExchangeClient *authlib.TokenExchangeClient
|
||||
// WrapTransport wraps the HTTP transport for authentication.
|
||||
// Takes precedence over TokenExchangeClient when both are set.
|
||||
// At least one of WrapTransport or TokenExchangeClient is required.
|
||||
WrapTransport transport.WrapperFunc
|
||||
// TLSClientConfig configures TLS for the client connection.
|
||||
TLSClientConfig clientrest.TLSClientConfig
|
||||
// QPS limits requests per second (defaults to DefaultQPS).
|
||||
QPS float32
|
||||
// Burst allows request bursts above QPS (defaults to DefaultBurst).
|
||||
Burst int
|
||||
// PageSize sets the number of items per API page (defaults to DefaultPageSize).
|
||||
PageSize int64
|
||||
}
|
||||
|
||||
// Setting represents the parsed spec of a Setting resource.
|
||||
type Setting struct {
|
||||
// Setting section
|
||||
Section string `json:"section"`
|
||||
// Setting key
|
||||
Key string `json:"key"`
|
||||
// Setting value
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// New creates a Service from the provided configuration.
|
||||
func New(config Config) (Service, error) {
|
||||
log := logging.New(LogPrefix)
|
||||
dynamicClient, err := getDynamicClient(config, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pageSize := DefaultPageSize
|
||||
if config.PageSize > 0 {
|
||||
pageSize = config.PageSize
|
||||
}
|
||||
|
||||
metrics := initMetrics()
|
||||
|
||||
return &remoteSettingService{
|
||||
dynamicClient: dynamicClient,
|
||||
pageSize: pageSize,
|
||||
log: log,
|
||||
metrics: metrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *remoteSettingService) ListAsIni(ctx context.Context, labelSelector metav1.LabelSelector) (*ini.File, error) {
|
||||
namespace, ok := request.NamespaceFrom(ctx)
|
||||
ns := semconv.GrafanaNamespaceName(namespace)
|
||||
ctx, span := tracer.Start(ctx, "remoteSettingService.ListAsIni",
|
||||
trace.WithAttributes(ns))
|
||||
defer span.End()
|
||||
|
||||
if !ok || namespace == "" {
|
||||
return nil, tracing.Errorf(span, "missing namespace in context")
|
||||
}
|
||||
|
||||
settings, err := m.List(ctx, labelSelector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iniFile, err := m.toIni(settings)
|
||||
if err != nil {
|
||||
return nil, tracing.Error(span, err)
|
||||
}
|
||||
return iniFile, nil
|
||||
}
|
||||
|
||||
func (m *remoteSettingService) List(ctx context.Context, labelSelector metav1.LabelSelector) ([]*Setting, error) {
|
||||
namespace, ok := request.NamespaceFrom(ctx)
|
||||
ns := semconv.GrafanaNamespaceName(namespace)
|
||||
ctx, span := tracer.Start(ctx, "remoteSettingService.List",
|
||||
trace.WithAttributes(ns))
|
||||
defer span.End()
|
||||
if !ok || namespace == "" {
|
||||
return nil, tracing.Errorf(span, "missing namespace in context")
|
||||
}
|
||||
log := m.log.FromContext(ctx).New(ns.Key, ns.Value, "function", "remoteSettingService.List", "traceId", span.SpanContext().TraceID())
|
||||
|
||||
startTime := time.Now()
|
||||
var status string
|
||||
defer func() {
|
||||
duration := time.Since(startTime).Seconds()
|
||||
m.metrics.listDuration.WithLabelValues(status).Observe(duration)
|
||||
}()
|
||||
|
||||
selector, err := metav1.LabelSelectorAsSelector(&labelSelector)
|
||||
if err != nil {
|
||||
status = "error"
|
||||
return nil, tracing.Error(span, err)
|
||||
}
|
||||
if selector.Empty() {
|
||||
log.Debug("empty selector. Fetching all settings")
|
||||
}
|
||||
|
||||
var allSettings []*Setting
|
||||
var continueToken string
|
||||
hasNext := true
|
||||
totalPages := 0
|
||||
// Using an upper limit to prevent infinite loops
|
||||
for hasNext && totalPages < 1000 {
|
||||
totalPages++
|
||||
opts := metav1.ListOptions{
|
||||
Limit: m.pageSize,
|
||||
Continue: continueToken,
|
||||
}
|
||||
if !selector.Empty() {
|
||||
opts.LabelSelector = selector.String()
|
||||
}
|
||||
|
||||
settingsList, lErr := m.dynamicClient.Resource(settingGroupVersion).Namespace(namespace).List(ctx, opts)
|
||||
if lErr != nil {
|
||||
status = "error"
|
||||
return nil, tracing.Error(span, lErr)
|
||||
}
|
||||
for i := range settingsList.Items {
|
||||
setting, pErr := parseSettingResource(&settingsList.Items[i])
|
||||
if pErr != nil {
|
||||
status = "error"
|
||||
return nil, tracing.Error(span, pErr)
|
||||
}
|
||||
allSettings = append(allSettings, setting)
|
||||
}
|
||||
continueToken = settingsList.GetContinue()
|
||||
if continueToken == "" {
|
||||
hasNext = false
|
||||
}
|
||||
}
|
||||
|
||||
status = "success"
|
||||
m.metrics.listResultSize.WithLabelValues(status).Observe(float64(len(allSettings)))
|
||||
|
||||
return allSettings, nil
|
||||
}
|
||||
|
||||
func parseSettingResource(setting *unstructured.Unstructured) (*Setting, error) {
|
||||
spec, found, err := unstructured.NestedMap(setting.Object, "spec")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get spec from setting: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("spec not found in setting %s", setting.GetName())
|
||||
}
|
||||
|
||||
var result Setting
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(spec, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to convert spec to Setting: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (m *remoteSettingService) toIni(settings []*Setting) (*ini.File, error) {
|
||||
conf := ini.Empty()
|
||||
for _, setting := range settings {
|
||||
if !conf.HasSection(setting.Section) {
|
||||
_, _ = conf.NewSection(setting.Section)
|
||||
}
|
||||
_, err := conf.Section(setting.Section).NewKey(setting.Key, setting.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func getDynamicClient(config Config, log logging.Logger) (dynamic.Interface, error) {
|
||||
if config.URL == "" {
|
||||
return nil, fmt.Errorf("URL cannot be empty")
|
||||
}
|
||||
if config.WrapTransport == nil && config.TokenExchangeClient == nil {
|
||||
return nil, fmt.Errorf("must set either TokenExchangeClient or WrapTransport")
|
||||
}
|
||||
|
||||
wrapTransport := config.WrapTransport
|
||||
if config.WrapTransport == nil {
|
||||
log.Debug("using default wrapTransport with TokenExchangeClient")
|
||||
wrapTransport = func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &authRoundTripper{
|
||||
tokenClient: config.TokenExchangeClient,
|
||||
transport: rt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qps := DefaultQPS
|
||||
if config.QPS > 0 {
|
||||
qps = config.QPS
|
||||
}
|
||||
|
||||
burst := DefaultBurst
|
||||
if config.Burst > 0 {
|
||||
burst = config.Burst
|
||||
}
|
||||
|
||||
return dynamic.NewForConfig(&clientrest.Config{
|
||||
Host: config.URL,
|
||||
WrapTransport: wrapTransport,
|
||||
TLSClientConfig: config.TLSClientConfig,
|
||||
QPS: qps,
|
||||
Burst: burst,
|
||||
})
|
||||
}
|
||||
|
||||
// authRoundTripper wraps an HTTP transport with token-based authentication.
|
||||
type authRoundTripper struct {
|
||||
tokenClient *authlib.TokenExchangeClient
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = (*authRoundTripper)(nil)
|
||||
|
||||
func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
token, err := a.tokenClient.Exchange(req.Context(), authlib.TokenExchangeRequest{
|
||||
Audiences: []string{ApiGroup},
|
||||
Namespace: "*",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to exchange token: %w", err)
|
||||
}
|
||||
req = utilnet.CloneRequest(req)
|
||||
|
||||
req.Header.Set("X-Access-Token", fmt.Sprintf("Bearer %s", token.Token))
|
||||
return a.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func initMetrics() remoteSettingServiceMetrics {
|
||||
metrics := remoteSettingServiceMetrics{
|
||||
listDuration: prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "settings",
|
||||
Subsystem: "service",
|
||||
Name: "list_settings_duration_seconds",
|
||||
Help: "Duration of remote settings service List operations",
|
||||
NativeHistogramBucketFactor: 1.1,
|
||||
},
|
||||
[]string{"status"}, // status: "success" or "error"
|
||||
),
|
||||
listResultSize: prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "settings",
|
||||
Subsystem: "service",
|
||||
Name: "list_settings_result_size",
|
||||
Help: "Number of settings returned by remote settings service List operations",
|
||||
NativeHistogramBucketFactor: 1.1,
|
||||
},
|
||||
[]string{"status"}, // status: "success" or "error"
|
||||
),
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func (m *remoteSettingService) Describe(descs chan<- *prometheus.Desc) {
|
||||
m.metrics.listDuration.Describe(descs)
|
||||
m.metrics.listResultSize.Describe(descs)
|
||||
}
|
||||
|
||||
func (m *remoteSettingService) Collect(metrics chan<- prometheus.Metric) {
|
||||
m.metrics.listDuration.Collect(metrics)
|
||||
m.metrics.listResultSize.Collect(metrics)
|
||||
}
|
||||
@@ -1,542 +0,0 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
k8testing "k8s.io/client-go/testing"
|
||||
|
||||
authlib "github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
func TestRemoteSettingService_ListAsIni(t *testing.T) {
|
||||
t.Run("should filter settings by label selector", func(t *testing.T) {
|
||||
// Create multiple settings, only some matching the selector
|
||||
setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"})
|
||||
setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"})
|
||||
setting3 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "host", Value: "localhost"})
|
||||
|
||||
client := newTestClient(500, setting1, setting2, setting3)
|
||||
|
||||
// Create a selector that should match only database settings
|
||||
selector := metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"section": "database",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
||||
result, err := client.ListAsIni(ctx, selector)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
// Should only have database settings, not server settings
|
||||
assert.True(t, result.HasSection("database"))
|
||||
assert.Equal(t, "postgres", result.Section("database").Key("type").String())
|
||||
assert.Equal(t, "localhost", result.Section("database").Key("host").String())
|
||||
// Should NOT have server settings
|
||||
assert.False(t, result.HasSection("server"))
|
||||
})
|
||||
|
||||
t.Run("should return all settings with empty selector", func(t *testing.T) {
|
||||
// Create multiple settings across different sections
|
||||
setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"})
|
||||
setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "mysql"})
|
||||
|
||||
client := newTestClient(500, setting1, setting2)
|
||||
|
||||
// Empty selector should select everything
|
||||
selector := metav1.LabelSelector{}
|
||||
|
||||
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
||||
result, err := client.ListAsIni(ctx, selector)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
// Should have all settings from all sections
|
||||
assert.True(t, result.HasSection("server"))
|
||||
assert.Equal(t, "3000", result.Section("server").Key("port").String())
|
||||
assert.True(t, result.HasSection("database"))
|
||||
assert.Equal(t, "mysql", result.Section("database").Key("type").String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoteSettingService_List(t *testing.T) {
|
||||
t.Run("should handle single page response", func(t *testing.T) {
|
||||
setting := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"})
|
||||
|
||||
client := newTestClient(500, setting)
|
||||
|
||||
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
||||
result, err := client.List(ctx, metav1.LabelSelector{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, 1)
|
||||
|
||||
spec := result[0]
|
||||
assert.Equal(t, "server", spec.Section)
|
||||
assert.Equal(t, "port", spec.Key)
|
||||
assert.Equal(t, "3000", spec.Value)
|
||||
})
|
||||
|
||||
t.Run("should handle multiple pages", func(t *testing.T) {
|
||||
totalPages := 3
|
||||
pageSize := 5
|
||||
|
||||
pages := make([][]*unstructured.Unstructured, totalPages)
|
||||
for pageNum := 0; pageNum < totalPages; pageNum++ {
|
||||
for idx := 0; idx < pageSize; idx++ {
|
||||
item := newUnstructuredSetting(
|
||||
"test-namespace",
|
||||
Setting{
|
||||
Section: fmt.Sprintf("section-%d", pageNum),
|
||||
Key: fmt.Sprintf("key-%d", idx),
|
||||
Value: fmt.Sprintf("val-%d-%d", pageNum, idx),
|
||||
},
|
||||
)
|
||||
pages[pageNum] = append(pages[pageNum], item)
|
||||
}
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
||||
listCallCount := 0
|
||||
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
||||
listCallCount++
|
||||
|
||||
continueToken := fmt.Sprintf("continue-%d", listCallCount)
|
||||
if listCallCount == totalPages {
|
||||
continueToken = ""
|
||||
}
|
||||
|
||||
if listCallCount <= totalPages {
|
||||
list := &unstructured.UnstructuredList{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": ApiGroup + "/" + apiVersion,
|
||||
"kind": listKind,
|
||||
},
|
||||
}
|
||||
list.SetContinue(continueToken)
|
||||
for _, item := range pages[listCallCount-1] {
|
||||
list.Items = append(list.Items, *item)
|
||||
}
|
||||
return true, list, nil
|
||||
}
|
||||
|
||||
return false, nil, nil
|
||||
})
|
||||
|
||||
client := &remoteSettingService{
|
||||
dynamicClient: dynamicClient,
|
||||
pageSize: int64(pageSize),
|
||||
log: log.NewNopLogger(),
|
||||
metrics: initMetrics(),
|
||||
}
|
||||
|
||||
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
||||
result, err := client.List(ctx, metav1.LabelSelector{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, totalPages*pageSize)
|
||||
assert.Equal(t, totalPages, listCallCount)
|
||||
})
|
||||
|
||||
t.Run("should pass label selector when provided", func(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
||||
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
||||
listAction := action.(k8testing.ListActionImpl)
|
||||
assert.Equal(t, "app=grafana", listAction.ListOptions.LabelSelector)
|
||||
return true, &unstructured.UnstructuredList{}, nil
|
||||
})
|
||||
|
||||
client := &remoteSettingService{
|
||||
dynamicClient: dynamicClient,
|
||||
pageSize: 500,
|
||||
log: log.NewNopLogger(),
|
||||
metrics: initMetrics(),
|
||||
}
|
||||
|
||||
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
||||
_, err := client.List(ctx, metav1.LabelSelector{MatchLabels: map[string]string{"app": "grafana"}})
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should stop pagination at 1000 pages", func(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
||||
listCallCount := 0
|
||||
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
||||
listCallCount++
|
||||
// Always return a continue token to simulate infinite pagination
|
||||
list := &unstructured.UnstructuredList{}
|
||||
list.SetContinue("continue-forever")
|
||||
return true, list, nil
|
||||
})
|
||||
|
||||
client := &remoteSettingService{
|
||||
dynamicClient: dynamicClient,
|
||||
pageSize: 10,
|
||||
log: log.NewNopLogger(),
|
||||
metrics: initMetrics(),
|
||||
}
|
||||
|
||||
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
||||
_, err := client.List(ctx, metav1.LabelSelector{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1000, listCallCount, "Should stop at 1000 pages to prevent infinite loops")
|
||||
})
|
||||
|
||||
t.Run("should return error when parsing setting fails", func(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind)
|
||||
dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) {
|
||||
// Return a malformed setting without spec
|
||||
list := &unstructured.UnstructuredList{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": ApiGroup + "/" + apiVersion,
|
||||
"kind": listKind,
|
||||
},
|
||||
}
|
||||
malformedSetting := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": ApiGroup + "/" + apiVersion,
|
||||
"kind": kind,
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "malformed",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
// Missing spec
|
||||
},
|
||||
}
|
||||
list.Items = append(list.Items, *malformedSetting)
|
||||
return true, list, nil
|
||||
})
|
||||
|
||||
client := &remoteSettingService{
|
||||
dynamicClient: dynamicClient,
|
||||
pageSize: 500,
|
||||
log: log.NewNopLogger(),
|
||||
metrics: initMetrics(),
|
||||
}
|
||||
|
||||
ctx := request.WithNamespace(context.Background(), "test-namespace")
|
||||
result, err := client.List(ctx, metav1.LabelSelector{})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "spec not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseSettingResource(t *testing.T) {
|
||||
t.Run("should parse valid setting resource", func(t *testing.T) {
|
||||
setting := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"})
|
||||
|
||||
result, err := parseSettingResource(setting)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "database", result.Section)
|
||||
assert.Equal(t, "type", result.Key)
|
||||
assert.Equal(t, "postgres", result.Value)
|
||||
})
|
||||
|
||||
t.Run("should return error when spec is missing", func(t *testing.T) {
|
||||
setting := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": ApiGroup + "/" + apiVersion,
|
||||
"kind": kind,
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-setting",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
// No spec
|
||||
},
|
||||
}
|
||||
|
||||
result, err := parseSettingResource(setting)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "spec not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoteSettingService_ToIni(t *testing.T) {
|
||||
t.Run("should convert settings to ini format", func(t *testing.T) {
|
||||
settings := []*Setting{
|
||||
{Section: "database", Key: "type", Value: "postgres"},
|
||||
{Section: "database", Key: "host", Value: "localhost"},
|
||||
{Section: "server", Key: "http_port", Value: "3000"},
|
||||
}
|
||||
|
||||
client := &remoteSettingService{
|
||||
pageSize: 500,
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
result, err := client.toIni(settings)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.True(t, result.HasSection("database"))
|
||||
assert.True(t, result.HasSection("server"))
|
||||
assert.Equal(t, "postgres", result.Section("database").Key("type").String())
|
||||
assert.Equal(t, "localhost", result.Section("database").Key("host").String())
|
||||
assert.Equal(t, "3000", result.Section("server").Key("http_port").String())
|
||||
})
|
||||
|
||||
t.Run("should handle empty settings list", func(t *testing.T) {
|
||||
var settings []*Setting
|
||||
|
||||
client := &remoteSettingService{
|
||||
pageSize: 500,
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
result, err := client.toIni(settings)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
sections := result.Sections()
|
||||
assert.Len(t, sections, 1) // Only default section
|
||||
})
|
||||
|
||||
t.Run("should create section if it does not exist", func(t *testing.T) {
|
||||
settings := []*Setting{
|
||||
{Section: "new_section", Key: "new_key", Value: "new_value"},
|
||||
}
|
||||
|
||||
client := &remoteSettingService{
|
||||
pageSize: 500,
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
result, err := client.toIni(settings)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.HasSection("new_section"))
|
||||
assert.Equal(t, "new_value", result.Section("new_section").Key("new_key").String())
|
||||
})
|
||||
|
||||
t.Run("should handle multiple keys in same section", func(t *testing.T) {
|
||||
settings := []*Setting{
|
||||
{Section: "auth", Key: "disable_login_form", Value: "false"},
|
||||
{Section: "auth", Key: "disable_signout_menu", Value: "true"},
|
||||
}
|
||||
|
||||
client := &remoteSettingService{
|
||||
pageSize: 500,
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
result, err := client.toIni(settings)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.HasSection("auth"))
|
||||
authSection := result.Section("auth")
|
||||
assert.Equal(t, "false", authSection.Key("disable_login_form").String())
|
||||
assert.Equal(t, "true", authSection.Key("disable_signout_menu").String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Run("should create client with default page size", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
||||
}
|
||||
|
||||
client, err := New(config)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, client)
|
||||
remoteClient := client.(*remoteSettingService)
|
||||
assert.Equal(t, DefaultPageSize, remoteClient.pageSize)
|
||||
})
|
||||
|
||||
t.Run("should create client with custom page size", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
||||
PageSize: 100,
|
||||
}
|
||||
|
||||
client, err := New(config)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, client)
|
||||
remoteClient := client.(*remoteSettingService)
|
||||
assert.Equal(t, int64(100), remoteClient.pageSize)
|
||||
})
|
||||
|
||||
t.Run("should use default page size when zero is provided", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
||||
PageSize: 0,
|
||||
}
|
||||
|
||||
client, err := New(config)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, client)
|
||||
remoteClient := client.(*remoteSettingService)
|
||||
assert.Equal(t, DefaultPageSize, remoteClient.pageSize)
|
||||
})
|
||||
|
||||
t.Run("should return error when config is invalid", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "", // Invalid: empty URL
|
||||
}
|
||||
|
||||
client, err := New(config)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "URL cannot be empty")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDynamicClient(t *testing.T) {
|
||||
logger := log.NewNopLogger()
|
||||
|
||||
t.Run("should return error when SettingServiceURL is empty", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "",
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
||||
}
|
||||
|
||||
client, err := getDynamicClient(config, logger)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "URL cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("should return error when both TokenExchangeClient and WrapTransport are nil", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
TokenExchangeClient: nil,
|
||||
WrapTransport: nil,
|
||||
}
|
||||
|
||||
client, err := getDynamicClient(config, logger)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "must set either TokenExchangeClient or WrapTransport")
|
||||
})
|
||||
|
||||
t.Run("should create client with WrapTransport", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
||||
}
|
||||
|
||||
client, err := getDynamicClient(config, logger)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, client)
|
||||
})
|
||||
|
||||
t.Run("should not fail when QPS and Burst are not provided", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
||||
}
|
||||
|
||||
client, err := getDynamicClient(config, logger)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, client)
|
||||
})
|
||||
|
||||
t.Run("should not fail when custom QPS and Burst are provided", func(t *testing.T) {
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt },
|
||||
QPS: 10.0,
|
||||
Burst: 20,
|
||||
}
|
||||
|
||||
client, err := getDynamicClient(config, logger)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, client)
|
||||
})
|
||||
|
||||
t.Run("should use WrapTransport when both WrapTransport and TokenExchangeClient are provided", func(t *testing.T) {
|
||||
wrapTransportCalled := false
|
||||
tokenExchangeClient := &authlib.TokenExchangeClient{}
|
||||
|
||||
config := Config{
|
||||
URL: "https://example.com",
|
||||
TokenExchangeClient: tokenExchangeClient,
|
||||
WrapTransport: func(rt http.RoundTripper) http.RoundTripper {
|
||||
wrapTransportCalled = true
|
||||
return rt
|
||||
},
|
||||
}
|
||||
|
||||
client, err := getDynamicClient(config, logger)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, client)
|
||||
assert.True(t, wrapTransportCalled, "WrapTransport should be called and take precedence over TokenExchangeClient")
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to create an unstructured Setting object for tests
|
||||
func newUnstructuredSetting(namespace string, spec Setting) *unstructured.Unstructured {
|
||||
// Generate resource name in the format {section}--{key}
|
||||
name := fmt.Sprintf("%s--%s", spec.Section, spec.Key)
|
||||
|
||||
obj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": ApiGroup + "/" + apiVersion,
|
||||
"kind": kind,
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
"namespace": namespace,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"section": spec.Section,
|
||||
"key": spec.Key,
|
||||
"value": spec.Value,
|
||||
},
|
||||
},
|
||||
}
|
||||
// Always set section and key labels
|
||||
obj.SetLabels(map[string]string{
|
||||
"section": spec.Section,
|
||||
"key": spec.Key,
|
||||
})
|
||||
return obj
|
||||
}
|
||||
|
||||
// Helper function to create a test client with the dynamic fake client
|
||||
func newTestClient(pageSize int64, objects ...runtime.Object) *remoteSettingService {
|
||||
scheme := runtime.NewScheme()
|
||||
dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind, objects...)
|
||||
|
||||
return &remoteSettingService{
|
||||
dynamicClient: dynamicClient,
|
||||
pageSize: pageSize,
|
||||
log: log.NewNopLogger(),
|
||||
metrics: initMetrics(),
|
||||
}
|
||||
}
|
||||
@@ -150,11 +150,6 @@ type Cfg struct {
|
||||
PluginsPath string
|
||||
EnterpriseLicensePath string
|
||||
|
||||
// Classic Provisioning settings
|
||||
ClassicProvisioningDashboardsServerLockMaxIntervalSeconds int64
|
||||
ClassicProvisioningDashboardsServerLockMinWaitMs int64
|
||||
ClassicProvisioningDashboardsServerLockMaxWaitMs int64
|
||||
|
||||
// SMTP email settings
|
||||
Smtp SmtpSettings
|
||||
|
||||
@@ -1226,8 +1221,6 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.readClassicProvisioningSettings(iniFile)
|
||||
|
||||
// read dashboard settings
|
||||
dashboards := iniFile.Section("dashboards")
|
||||
cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20)
|
||||
@@ -2114,12 +2107,6 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readClassicProvisioningSettings(iniFile *ini.File) {
|
||||
cfg.ClassicProvisioningDashboardsServerLockMinWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_min_wait_ms").MustInt64(100)
|
||||
cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_wait_ms").MustInt64(1000)
|
||||
cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_interval_seconds").MustInt64(15)
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error {
|
||||
provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
|
||||
cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath)
|
||||
|
||||
@@ -202,58 +202,30 @@ then run:
|
||||
kubectl --kubeconfig=./grafana.kubeconfig create -f folder-generate.yaml
|
||||
```
|
||||
|
||||
### Run as a separate GRPC service
|
||||
### Run as a GRPC service
|
||||
|
||||
It is recommended to use a separate config file for the storage-server. Create a file `conf/storage-server.ini` with the following content:
|
||||
#### Start GRPC storage-server
|
||||
|
||||
Make sure you have the gRPC address in the `[grafana-apiserver]` section of your config file:
|
||||
```ini
|
||||
app_mode = development
|
||||
|
||||
target = storage-server
|
||||
|
||||
[database]
|
||||
type = mysql
|
||||
host = 127.0.0.1:3306
|
||||
name = unified-storage
|
||||
user = root
|
||||
password = rootpass
|
||||
skip_migrations = true
|
||||
ensure_default_org_and_user = false
|
||||
|
||||
[grpc_server]
|
||||
network = "tcp"
|
||||
address = "127.0.0.1:10000"
|
||||
|
||||
[grafana-apiserver]
|
||||
storage_type = unified
|
||||
; your gRPC server address
|
||||
address = localhost:10000
|
||||
```
|
||||
|
||||
You also need the `[grpc_server_authentication]` section to authenticate incoming requests:
|
||||
```ini
|
||||
[grpc_server_authentication]
|
||||
signing_keys_url = http://localhost:3011/api/signing-keys/keys
|
||||
; http url to Grafana's signing keys to validate incoming id tokens
|
||||
signing_keys_url = http://localhost:3000/api/signing-keys/keys
|
||||
mode = "on-prem"
|
||||
|
||||
[feature_toggles]
|
||||
kubernetesDashboards = true
|
||||
kubernetesFolders = true
|
||||
unifiedStorage = true
|
||||
unifiedStorageHistoryPruner = true
|
||||
unifiedStorageSearch = true
|
||||
unifiedStorageSearchPermissionFiltering = false
|
||||
unifiedStorageSearchSprinkles = false
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
https_skip_verify = true
|
||||
```
|
||||
|
||||
You should also have a MySQL database running. You can create one with our docker blocks by running:
|
||||
```bash
|
||||
make devenv sources=mysql
|
||||
```
|
||||
The database credentials in the example above will work with the default mysql docker block. You'll also need to create a database named `unified-storage`.
|
||||
This currently only works with a separate database configuration (see previous section).
|
||||
|
||||
Start the storage-server with:
|
||||
```sh
|
||||
./bin/grafana server target --config conf/storage-server.ini
|
||||
GF_DEFAULT_TARGET=storage-server ./bin/grafana server target
|
||||
```
|
||||
|
||||
The GRPC service will listen on port 10000
|
||||
|
||||
@@ -184,6 +184,7 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req
|
||||
return true
|
||||
}, claims.NoopZookie{}, nil
|
||||
}
|
||||
|
||||
if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) {
|
||||
span.SetAttributes(attribute.Bool("allowed", false))
|
||||
span.SetStatus(codes.Error, "Namespace mismatch")
|
||||
|
||||
@@ -252,7 +252,7 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
||||
"rules": [{
|
||||
"state": "inactive",
|
||||
"name": "AlwaysFiring",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||
"duration": 10,
|
||||
"folderUid": "default",
|
||||
"uid": "%s",
|
||||
@@ -270,7 +270,7 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
||||
}, {
|
||||
"state": "inactive",
|
||||
"name": "AlwaysFiringButSilenced",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||
"folderUid": "default",
|
||||
"uid": "%s",
|
||||
"health": "ok",
|
||||
@@ -317,7 +317,7 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
||||
"rules": [{
|
||||
"state": "inactive",
|
||||
"name": "AlwaysFiring",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||
"duration": 10,
|
||||
"folderUid": "default",
|
||||
"uid": "%s",
|
||||
@@ -335,7 +335,7 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
||||
}, {
|
||||
"state": "inactive",
|
||||
"name": "AlwaysFiringButSilenced",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||
"folderUid": "default",
|
||||
"uid": "%s",
|
||||
"health": "ok",
|
||||
@@ -639,7 +639,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
||||
"name": "AlwaysFiring",
|
||||
"uid": "%s",
|
||||
"folderUid": "default",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||
"duration": 10,
|
||||
"keepFiringFor": 15,
|
||||
"annotations": {
|
||||
@@ -656,7 +656,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
||||
"name": "AlwaysFiringButSilenced",
|
||||
"uid": "%s",
|
||||
"folderUid": "default",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||
"health": "ok",
|
||||
"isPaused": false,
|
||||
"type": "alerting",
|
||||
@@ -688,7 +688,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
||||
"name": "AlwaysFiring",
|
||||
"uid": "%s",
|
||||
"folderUid": "default",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]",
|
||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||
"duration": 10,
|
||||
"keepFiringFor": 15,
|
||||
"annotations": {
|
||||
|
||||
@@ -1166,7 +1166,6 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) {
|
||||
"expression": "2 + 3 \u003e 1",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}],
|
||||
@@ -1210,7 +1209,6 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) {
|
||||
"expression": "2 + 3 \u003e 1",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}],
|
||||
@@ -1266,7 +1264,6 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) {
|
||||
"expression": "2 + 3 \u003e 1",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}],
|
||||
@@ -1613,7 +1610,7 @@ func TestIntegrationRuleCreate(t *testing.T) {
|
||||
To: apimodels.Duration(15 * time.Minute),
|
||||
},
|
||||
DatasourceUID: expr.DatasourceUID,
|
||||
Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}`),
|
||||
Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"type":"math"}`),
|
||||
},
|
||||
},
|
||||
UpdatedBy: &apimodels.UserInfo{
|
||||
@@ -2684,7 +2681,6 @@ func TestIntegrationQuota(t *testing.T) {
|
||||
"expression":"2 + 4 \u003E 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
@@ -2802,7 +2798,6 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) {
|
||||
"expression": "2 + 3 > 1",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}
|
||||
@@ -3290,7 +3285,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
||||
"expression":"2 + 3 \u003e 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
@@ -3337,7 +3331,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
||||
"expression":"2 + 3 \u003e 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
@@ -3690,7 +3683,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
||||
"expression":"2 + 3 \u003e 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
@@ -3737,7 +3729,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
||||
"expression":"2 + 3 \u003e 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
@@ -3881,7 +3872,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
||||
"expression":"2 + 3 \u003C 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
@@ -4005,7 +3995,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
||||
"expression":"2 + 3 \u003C 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
@@ -4104,7 +4093,6 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
|
||||
"expression":"2 + 3 \u003C 1",
|
||||
"intervalMs":1000,
|
||||
"maxDataPoints":43200,
|
||||
"refId":"A",
|
||||
"type":"math"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"expression": "0 \u003e 0",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}
|
||||
@@ -56,7 +55,6 @@
|
||||
"expression": "0 == 0",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"expression": "0/0",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"expression": "0 \u003e 0",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}
|
||||
@@ -55,7 +54,6 @@
|
||||
"expression": "0 == 0",
|
||||
"intervalMs": 1000,
|
||||
"maxDataPoints": 43200,
|
||||
"refId": "A",
|
||||
"type": "math"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ func TestIntegrationPluginMeta(t *testing.T) {
|
||||
}, &pluginsv0alpha1.PluginMeta{})
|
||||
|
||||
require.NotNil(t, response.Result)
|
||||
require.NotNil(t, response.Result.Spec.PluginJson)
|
||||
require.Equal(t, "grafana-piechart-panel", response.Result.Spec.PluginJson.Id)
|
||||
require.NotEmpty(t, response.Result.Spec.PluginJson.Name)
|
||||
require.NotEmpty(t, response.Result.Spec.PluginJson.Type)
|
||||
require.NotNil(t, response.Result.Spec.PluginJSON)
|
||||
require.Equal(t, "grafana-piechart-panel", response.Result.Spec.PluginJSON.Id)
|
||||
require.NotEmpty(t, response.Result.Spec.PluginJSON.Name)
|
||||
require.NotEmpty(t, response.Result.Spec.PluginJSON.Type)
|
||||
})
|
||||
|
||||
t.Run("get plugin meta for non-existent plugin", func(t *testing.T) {
|
||||
|
||||
@@ -58,11 +58,11 @@ func TestIntegrationPluginMetas(t *testing.T) {
|
||||
|
||||
foundIDs := make(map[string]bool)
|
||||
for _, item := range response.Result.Items {
|
||||
require.NotNil(t, item.Spec.PluginJson)
|
||||
foundIDs[item.Spec.PluginJson.Id] = true
|
||||
require.NotEmpty(t, item.Spec.PluginJson.Id)
|
||||
require.NotEmpty(t, item.Spec.PluginJson.Type)
|
||||
require.NotEmpty(t, item.Spec.PluginJson.Name)
|
||||
require.NotNil(t, item.Spec.PluginJSON)
|
||||
foundIDs[item.Spec.PluginJSON.Id] = true
|
||||
require.NotEmpty(t, item.Spec.PluginJSON.Id)
|
||||
require.NotEmpty(t, item.Spec.PluginJSON.Type)
|
||||
require.NotEmpty(t, item.Spec.PluginJSON.Name)
|
||||
}
|
||||
require.True(t, foundIDs["grafana-piechart-panel"])
|
||||
require.True(t, foundIDs["grafana-clock-panel"])
|
||||
|
||||
@@ -11,7 +11,6 @@ import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
locationUtil,
|
||||
monacoLanguageRegistry,
|
||||
PluginMetasResponse,
|
||||
setLocale,
|
||||
setTimeZoneResolver,
|
||||
setWeekStart,
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
setCorrelationsService,
|
||||
setPluginFunctionsHook,
|
||||
setMegaMenuOpenHook,
|
||||
GrafanaBootConfig,
|
||||
} from '@grafana/runtime';
|
||||
import {
|
||||
initOpenFeature,
|
||||
@@ -259,28 +257,6 @@ export class GrafanaApp {
|
||||
const skipAppPluginsPreload =
|
||||
config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render';
|
||||
if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) {
|
||||
const response = await backendSrv.get<PluginMetasResponse>(
|
||||
`/apis/plugins.grafana.app/v0alpha1/namespaces/${config.namespace}/pluginmetas`
|
||||
);
|
||||
const plugins: GrafanaBootConfig['plugins'] = { apps: {}, panels: {}, datasources: {} };
|
||||
response.items.reduce((acc, curr) => {
|
||||
if (curr.spec.pluginJson.type === 'app') {
|
||||
acc.apps[curr.spec.pluginJson.id] = curr.spec;
|
||||
}
|
||||
|
||||
if (curr.spec.pluginJson.type === 'panel') {
|
||||
acc.panels[curr.spec.pluginJson.id] = curr.spec;
|
||||
}
|
||||
|
||||
if (curr.spec.pluginJson.type === 'datasource') {
|
||||
acc.datasources[curr.spec.pluginJson.id] = curr.spec;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, plugins);
|
||||
|
||||
updateConfig({ plugins });
|
||||
|
||||
const appPluginsToAwait = getAppPluginsToAwait();
|
||||
const appPluginsToPreload = getAppPluginsToPreload();
|
||||
|
||||
|
||||
@@ -138,26 +138,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
||||
range: () => {
|
||||
const state = builder.getState();
|
||||
if (state.isPanning) {
|
||||
if (state.isTimeRangePending) {
|
||||
const timeRange = getTimeRange();
|
||||
const propsFrom = timeRange.from.valueOf();
|
||||
const propsTo = timeRange.to.valueOf();
|
||||
|
||||
const MIN_TIMESPAN_MS = 1;
|
||||
const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS;
|
||||
const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS;
|
||||
const timeRangeHasUpdated = fromMatches && toMatches;
|
||||
|
||||
if (timeRangeHasUpdated) {
|
||||
builder.setState({ isPanning: false });
|
||||
return [propsFrom, propsTo];
|
||||
}
|
||||
}
|
||||
|
||||
return [state.min, state.max];
|
||||
}
|
||||
const timeRange = getTimeRange();
|
||||
return [timeRange.from.valueOf(), timeRange.to.valueOf()];
|
||||
const r = getTimeRange();
|
||||
return [r.from.valueOf(), r.to.valueOf()];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -159,24 +159,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
||||
range: (u) => {
|
||||
const state = builder.getState();
|
||||
if (state.isPanning) {
|
||||
if (state.isTimeRangePending) {
|
||||
const propsRange = coreConfig.xRange(u);
|
||||
const propsFrom = propsRange[0];
|
||||
const propsTo = propsRange[1];
|
||||
|
||||
if (propsFrom != null && propsTo != null) {
|
||||
const MIN_TIMESPAN_MS = 1;
|
||||
const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS;
|
||||
const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS;
|
||||
const timeRangeHasUpdated = fromMatches && toMatches;
|
||||
|
||||
if (timeRangeHasUpdated) {
|
||||
builder.setState({ isPanning: false });
|
||||
return propsRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [state.min, state.max];
|
||||
}
|
||||
return coreConfig.xRange(u);
|
||||
|
||||
@@ -22,7 +22,7 @@ jest.mock('./scopesUtils', () => {
|
||||
});
|
||||
|
||||
const mockScopeServicesState = {
|
||||
filterNode: jest.fn(),
|
||||
updateNode: jest.fn(),
|
||||
selectScope: jest.fn(),
|
||||
resetSelection: jest.fn(),
|
||||
nodes: {},
|
||||
@@ -99,12 +99,12 @@ describe('useRegisterScopesActions', () => {
|
||||
});
|
||||
|
||||
it('should register scope tree actions and return scopesRow when scopes are selected', () => {
|
||||
const mockFilterNode = jest.fn();
|
||||
const mockUpdateNode = jest.fn();
|
||||
|
||||
// First run with empty scopes in the scopes service
|
||||
(useScopeServicesState as jest.Mock).mockReturnValue({
|
||||
...mockScopeServicesState,
|
||||
filterNode: mockFilterNode,
|
||||
updateNode: mockUpdateNode,
|
||||
selectedScopes: [{ scopeId: 'scope1', name: 'Scope 1' }],
|
||||
});
|
||||
|
||||
@@ -112,14 +112,14 @@ describe('useRegisterScopesActions', () => {
|
||||
return useRegisterScopesActions('', jest.fn());
|
||||
});
|
||||
|
||||
expect(mockFilterNode).toHaveBeenCalledWith('', '');
|
||||
expect(mockUpdateNode).toHaveBeenCalledWith('', true, '');
|
||||
expect(useRegisterActions).toHaveBeenLastCalledWith([rootScopeAction], [[rootScopeAction]]);
|
||||
expect(result.current.scopesRow).toBeDefined();
|
||||
|
||||
// Simulate loading of scopes in the service
|
||||
(useScopeServicesState as jest.Mock).mockReturnValue({
|
||||
...mockScopeServicesState,
|
||||
filterNode: mockFilterNode,
|
||||
updateNode: mockUpdateNode,
|
||||
selectedScopes: [{ scopeId: 'scope1', name: 'Scope 1' }],
|
||||
nodes,
|
||||
tree,
|
||||
@@ -151,12 +151,12 @@ describe('useRegisterScopesActions', () => {
|
||||
});
|
||||
|
||||
it('should load next level of scopes', () => {
|
||||
const mockFilterNode = jest.fn();
|
||||
const mockUpdateNode = jest.fn();
|
||||
|
||||
// First run with empty scopes in the scopes service
|
||||
(useScopeServicesState as jest.Mock).mockReturnValue({
|
||||
...mockScopeServicesState,
|
||||
filterNode: mockFilterNode,
|
||||
updateNode: mockUpdateNode,
|
||||
nodes,
|
||||
tree,
|
||||
});
|
||||
@@ -165,7 +165,7 @@ describe('useRegisterScopesActions', () => {
|
||||
return useRegisterScopesActions('', jest.fn(), 'scopes/scope1');
|
||||
});
|
||||
|
||||
expect(mockFilterNode).toHaveBeenCalledWith('scope1', '');
|
||||
expect(mockUpdateNode).toHaveBeenCalledWith('scope1', true, '');
|
||||
});
|
||||
|
||||
it('does not return component if no scopes are selected', () => {
|
||||
@@ -259,12 +259,12 @@ describe('useRegisterScopesActions', () => {
|
||||
});
|
||||
|
||||
it('should not use global scope search when searching in some deeper scope category', async () => {
|
||||
const mockFilterNode = jest.fn();
|
||||
const mockUpdateNode = jest.fn();
|
||||
|
||||
// First run with empty scopes in the scopes service
|
||||
(useScopeServicesState as jest.Mock).mockReturnValue({
|
||||
...mockScopeServicesState,
|
||||
filterNode: mockFilterNode,
|
||||
updateNode: mockUpdateNode,
|
||||
nodes,
|
||||
tree,
|
||||
});
|
||||
@@ -273,17 +273,17 @@ describe('useRegisterScopesActions', () => {
|
||||
return useRegisterScopesActions('something', jest.fn(), 'scopes/scope1');
|
||||
});
|
||||
|
||||
expect(mockFilterNode).toHaveBeenCalledWith('scope1', 'something');
|
||||
expect(mockUpdateNode).toHaveBeenCalledWith('scope1', true, 'something');
|
||||
expect(mockScopeServicesState.searchAllNodes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not use global scope search if feature flag is off', async () => {
|
||||
config.featureToggles.scopeSearchAllLevels = false;
|
||||
const mockFilterNode = jest.fn();
|
||||
const mockUpdateNode = jest.fn();
|
||||
// First run with empty scopes in the scopes service
|
||||
(useScopeServicesState as jest.Mock).mockReturnValue({
|
||||
...mockScopeServicesState,
|
||||
filterNode: mockFilterNode,
|
||||
updateNode: mockUpdateNode,
|
||||
nodes,
|
||||
tree,
|
||||
});
|
||||
@@ -292,7 +292,7 @@ describe('useRegisterScopesActions', () => {
|
||||
return useRegisterScopesActions('something', jest.fn(), '');
|
||||
});
|
||||
|
||||
expect(mockFilterNode).toHaveBeenCalledWith('', 'something');
|
||||
expect(mockUpdateNode).toHaveBeenCalledWith('', true, 'something');
|
||||
expect(mockScopeServicesState.searchAllNodes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -58,22 +58,22 @@ export function useRegisterScopesActions(
|
||||
* @param parentId
|
||||
*/
|
||||
function useScopeTreeActions(searchQuery: string, parentId?: string | null) {
|
||||
const { filterNode, selectScope, resetSelection, nodes, tree, selectedScopes } = useScopeServicesState();
|
||||
const { updateNode, selectScope, resetSelection, nodes, tree, selectedScopes } = useScopeServicesState();
|
||||
|
||||
// Initialize the scopes the first time this runs and reset the scopes that were selected on unmount.
|
||||
useEffect(() => {
|
||||
filterNode('', '');
|
||||
updateNode('', true, '');
|
||||
resetSelection();
|
||||
return () => {
|
||||
resetSelection();
|
||||
};
|
||||
}, [filterNode, resetSelection]);
|
||||
}, [updateNode, resetSelection]);
|
||||
|
||||
// Load the next level of scopes when the parentId changes.
|
||||
useEffect(() => {
|
||||
const parentScopeId = !parentId || parentId === 'scopes' ? '' : last(parentId.split('/'))!;
|
||||
filterNode(parentScopeId, searchQuery);
|
||||
}, [filterNode, searchQuery, parentId]);
|
||||
updateNode(parentScopeId, true, searchQuery);
|
||||
}, [updateNode, searchQuery, parentId]);
|
||||
|
||||
return useMemo(
|
||||
() => mapScopesNodesTreeToActions(nodes, tree!, selectedScopes, selectScope),
|
||||
|
||||
@@ -14,7 +14,7 @@ export function useScopeServicesState() {
|
||||
const services = useScopesServices();
|
||||
if (!services) {
|
||||
return {
|
||||
filterNode: () => Promise.resolve(),
|
||||
updateNode: () => {},
|
||||
selectScope: () => {},
|
||||
resetSelection: () => {},
|
||||
searchAllNodes: () => Promise.resolve([]),
|
||||
@@ -32,7 +32,7 @@ export function useScopeServicesState() {
|
||||
},
|
||||
};
|
||||
}
|
||||
const { filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply, getScopeNodes } =
|
||||
const { updateNode, filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply, getScopeNodes } =
|
||||
services.scopesSelectorService;
|
||||
const selectorServiceState: ScopesSelectorServiceState | undefined = useObservable(
|
||||
services.scopesSelectorService.stateObservable ?? new Observable(),
|
||||
@@ -42,6 +42,7 @@ export function useScopeServicesState() {
|
||||
return {
|
||||
getScopeNodes,
|
||||
filterNode,
|
||||
updateNode,
|
||||
selectScope,
|
||||
resetSelection,
|
||||
searchAllNodes,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
import { Box, Icon, Sidebar, Text, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
import { Box, Icon, Sidebar, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
@@ -85,7 +85,6 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }
|
||||
aria-selected={isSelected}
|
||||
className={styles.container}
|
||||
onClick={onNodeClicked}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
style={{ '--depth': depth } as React.CSSProperties}
|
||||
>
|
||||
<div className={cx(styles.row, { [styles.rowSelected]: isSelected })}>
|
||||
@@ -100,7 +99,7 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={cx(styles.nodeButton, { [styles.nodeButtonClone]: isCloned })}
|
||||
className={cx(styles.nodeName, { [styles.nodeNameClone]: isCloned })}
|
||||
onDoubleClick={outlineRename.onNameDoubleClicked}
|
||||
data-testid={selectors.components.PanelEditor.Outline.item(instanceName)}
|
||||
>
|
||||
@@ -117,10 +116,10 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.nodeName}>
|
||||
<Text truncate>{instanceName}</Text>
|
||||
<Stack direction="row" gap={0.5} alignItems="center" grow={1}>
|
||||
<span>{instanceName}</span>
|
||||
{elementInfo.isHidden && <Icon name="eye-slash" size="sm" className={styles.hiddenIcon} />}
|
||||
</div>
|
||||
</Stack>
|
||||
{isCloned && (
|
||||
<span>
|
||||
<Trans i18nKey="dashboard.outline.repeated-item">Repeat</Trans>
|
||||
@@ -145,20 +144,9 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<li
|
||||
role="treeitem"
|
||||
aria-selected={isSelected}
|
||||
className={styles.container}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
style={{ '--depth': depth + 1 } as React.CSSProperties}
|
||||
>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.indentation}></div>
|
||||
<Text color="secondary" italic>
|
||||
<Trans i18nKey="dashboard.outline.tree-item.empty">(empty)</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
</li>
|
||||
<Text color="secondary" element="li">
|
||||
<Trans i18nKey="dashboard.outline.tree-item.empty">(empty)</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
@@ -209,7 +197,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
color: 'inherit',
|
||||
lineHeight: 0,
|
||||
}),
|
||||
nodeButton: css({
|
||||
nodeName: css({
|
||||
boxShadow: 'none',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
@@ -227,18 +215,11 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}),
|
||||
nodeName: css({
|
||||
display: 'flex',
|
||||
gap: theme.spacing(0.5),
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
hiddenIcon: css({
|
||||
color: theme.colors.text.secondary,
|
||||
marginLeft: theme.spacing(1),
|
||||
}),
|
||||
nodeButtonClone: css({
|
||||
nodeNameClone: css({
|
||||
color: theme.colors.text.secondary,
|
||||
cursor: 'not-allowed',
|
||||
}),
|
||||
|
||||
@@ -272,7 +272,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
gap: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
float: 'right',
|
||||
alignItems: 'flex-start',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
timeControls: css({
|
||||
display: 'flex',
|
||||
|
||||
@@ -68,16 +68,6 @@ const defaultMatchers: LocationFieldMatchers = {
|
||||
geo: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.geo),
|
||||
};
|
||||
|
||||
/**
|
||||
* suggestions needs to run sync, and we just want to use the default matchers in that situation.
|
||||
*/
|
||||
export function getDefaultLocationMatchers(): LocationFieldMatchers {
|
||||
return {
|
||||
...defaultMatchers,
|
||||
mode: FrameGeometrySourceMode.Auto,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLocationMatchers(src?: FrameGeometrySource): Promise<LocationFieldMatchers> {
|
||||
const info: LocationFieldMatchers = {
|
||||
...defaultMatchers,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { css } from '@emotion/css';
|
||||
|
||||
import {
|
||||
CoreApp,
|
||||
FieldType,
|
||||
getPanelDataSummary,
|
||||
GrafanaTheme2,
|
||||
PanelDataSummary,
|
||||
@@ -135,15 +134,15 @@ function getMessageFor(
|
||||
return fieldConfig?.defaults.noValue ?? t('panel.panel-data-error-view.no-value.default', 'No data');
|
||||
}
|
||||
|
||||
if (needsStringField && !dataSummary.hasFieldType(FieldType.string)) {
|
||||
if (needsStringField && !dataSummary.hasStringField) {
|
||||
return t('panel.panel-data-error-view.missing-value.string', 'Data is missing a string field');
|
||||
}
|
||||
|
||||
if (needsNumberField && !dataSummary.hasFieldType(FieldType.number)) {
|
||||
if (needsNumberField && !dataSummary.hasNumberField) {
|
||||
return t('panel.panel-data-error-view.missing-value.number', 'Data is missing a number field');
|
||||
}
|
||||
|
||||
if (needsTimeField && !dataSummary.hasFieldType(FieldType.time)) {
|
||||
if (needsTimeField && !dataSummary.hasTimeField) {
|
||||
return t('panel.panel-data-error-view.missing-value.time', 'Data is missing a time field');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,18 +5,12 @@ import {
|
||||
LoadingState,
|
||||
PanelData,
|
||||
PanelPluginMeta,
|
||||
PanelPluginVisualizationSuggestion,
|
||||
toDataFrame,
|
||||
PanelPluginVisualizationSuggestion,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
BarGaugeDisplayMode,
|
||||
BigValueColorMode,
|
||||
GraphFieldConfig,
|
||||
ReduceDataOptions,
|
||||
StackingMode,
|
||||
VizOrientation,
|
||||
} from '@grafana/schema';
|
||||
import { GraphFieldConfig, ReduceDataOptions } from '@grafana/schema';
|
||||
import { config } from 'app/core/config';
|
||||
import { SuggestionName } from 'app/types/suggestions';
|
||||
|
||||
import { getAllSuggestions, panelsToCheckFirst } from './getAllSuggestions';
|
||||
|
||||
@@ -27,8 +21,6 @@ for (const pluginId of panelsToCheckFirst) {
|
||||
} as PanelPluginMeta;
|
||||
}
|
||||
|
||||
const SCALAR_PLUGINS = ['gauge', 'stat', 'bargauge', 'piechart', 'radialbar'];
|
||||
|
||||
config.panels['text'] = {
|
||||
id: 'text',
|
||||
name: 'Text',
|
||||
@@ -77,10 +69,7 @@ scenario('No series', (ctx) => {
|
||||
ctx.setData([]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([
|
||||
expect.objectContaining({ pluginId: 'table' }),
|
||||
expect.objectContaining({ pluginId: 'text' }),
|
||||
]);
|
||||
expect(ctx.names()).toEqual([SuggestionName.Table, SuggestionName.TextPanel]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,7 +84,7 @@ scenario('No rows', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([expect.objectContaining({ pluginId: 'table' })]);
|
||||
expect(ctx.names()).toEqual([SuggestionName.Table]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,46 +100,33 @@ scenario('Single frame with time and number field', (ctx) => {
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart - smooth' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart' }),
|
||||
expect.objectContaining({ pluginId: 'gauge' }),
|
||||
expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }),
|
||||
expect.objectContaining({ pluginId: 'stat' }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'stat',
|
||||
options: expect.objectContaining({ colorMode: BigValueColorMode.Background }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'table' }),
|
||||
expect.objectContaining({ name: SuggestionName.LineChart }),
|
||||
expect.objectContaining({ name: SuggestionName.LineChartSmooth }),
|
||||
expect.objectContaining({ name: SuggestionName.AreaChart }),
|
||||
expect.objectContaining({ name: SuggestionName.LineChartGradientColorScheme }),
|
||||
expect.objectContaining({ name: SuggestionName.BarChart }),
|
||||
expect.objectContaining({ name: SuggestionName.BarChartGradientColorScheme }),
|
||||
expect.objectContaining({ name: SuggestionName.Gauge }),
|
||||
expect.objectContaining({ name: SuggestionName.GaugeNoThresholds }),
|
||||
expect.objectContaining({ name: SuggestionName.Stat }),
|
||||
expect.objectContaining({ name: SuggestionName.StatColoredBackground }),
|
||||
expect.objectContaining({ name: SuggestionName.BarGaugeBasic }),
|
||||
expect.objectContaining({ name: SuggestionName.BarGaugeLCD }),
|
||||
expect.objectContaining({ name: SuggestionName.Table }),
|
||||
expect.objectContaining({ pluginId: 'state-timeline' }),
|
||||
expect.objectContaining({ pluginId: 'status-history' }),
|
||||
expect.objectContaining({ pluginId: 'heatmap' }),
|
||||
expect.objectContaining({ pluginId: 'histogram' }),
|
||||
expect.objectContaining({ name: SuggestionName.StatusHistory }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Bar chart suggestion should be using timeseries panel', () => {
|
||||
expect(ctx.suggestions.find((x) => x.name === 'Bar chart')?.pluginId).toBe('timeseries');
|
||||
expect(ctx.suggestions.find((x) => x.name === SuggestionName.BarChart)?.pluginId).toBe('timeseries');
|
||||
});
|
||||
|
||||
it('Scalar panels should use calcs', () => {
|
||||
for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) {
|
||||
expect(suggestion).toEqual(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
reduceOptions: expect.objectContaining({ values: false, calcs: ['lastNotNull'] }),
|
||||
}),
|
||||
})
|
||||
);
|
||||
it('Stat panels have reduce values disabled', () => {
|
||||
for (const suggestion of ctx.suggestions) {
|
||||
if (suggestion.options?.reduceOptions?.values) {
|
||||
throw new Error(`Suggestion ${suggestion.name} reduce.values set to true when it should be false`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -168,46 +144,31 @@ scenario('Single frame with time 2 number fields', (ctx) => {
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart - smooth' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart - stacked' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart - stacked by percentage' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart - stacked' }),
|
||||
expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart - stacked by percentage' }),
|
||||
expect.objectContaining({ pluginId: 'gauge' }),
|
||||
expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }),
|
||||
expect.objectContaining({ pluginId: 'stat' }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'stat',
|
||||
options: expect.objectContaining({ colorMode: BigValueColorMode.Background }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'piechart' }),
|
||||
expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'table' }),
|
||||
expect.objectContaining({ name: SuggestionName.LineChart }),
|
||||
expect.objectContaining({ name: SuggestionName.LineChartSmooth }),
|
||||
expect.objectContaining({ name: SuggestionName.AreaChartStacked }),
|
||||
expect.objectContaining({ name: SuggestionName.AreaChartStackedPercent }),
|
||||
expect.objectContaining({ name: SuggestionName.BarChartStacked }),
|
||||
expect.objectContaining({ name: SuggestionName.BarChartStackedPercent }),
|
||||
expect.objectContaining({ name: SuggestionName.Gauge }),
|
||||
expect.objectContaining({ name: SuggestionName.GaugeNoThresholds }),
|
||||
expect.objectContaining({ name: SuggestionName.Stat }),
|
||||
expect.objectContaining({ name: SuggestionName.StatColoredBackground }),
|
||||
expect.objectContaining({ name: SuggestionName.PieChart }),
|
||||
expect.objectContaining({ name: SuggestionName.PieChartDonut }),
|
||||
expect.objectContaining({ name: SuggestionName.BarGaugeBasic }),
|
||||
expect.objectContaining({ name: SuggestionName.BarGaugeLCD }),
|
||||
expect.objectContaining({ name: SuggestionName.Table }),
|
||||
expect.objectContaining({ pluginId: 'state-timeline' }),
|
||||
expect.objectContaining({ pluginId: 'status-history' }),
|
||||
expect.objectContaining({ pluginId: 'heatmap' }),
|
||||
expect.objectContaining({ pluginId: 'histogram' }),
|
||||
expect.objectContaining({ name: SuggestionName.StatusHistory }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Scalar panels should use calcs', () => {
|
||||
for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) {
|
||||
expect(suggestion).toEqual(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
reduceOptions: expect.objectContaining({ values: false, calcs: ['lastNotNull'] }),
|
||||
}),
|
||||
})
|
||||
);
|
||||
it('Stat panels have reduceOptions.values disabled', () => {
|
||||
for (const suggestion of ctx.suggestions) {
|
||||
if (suggestion.options?.reduceOptions?.values) {
|
||||
throw new Error(`Suggestion ${suggestion.name} reduce.values set to true when it should be false`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -223,7 +184,7 @@ scenario('Single time series with 100 data points', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should not suggest bar chart', () => {
|
||||
expect(ctx.suggestions.find((x) => x.name === 'Bar chart')).toBe(undefined);
|
||||
expect(ctx.suggestions.find((x) => x.name === SuggestionName.BarChart)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -274,42 +235,26 @@ scenario('Single frame with string and number field', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([
|
||||
expect.objectContaining({ pluginId: 'piechart' }),
|
||||
expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }),
|
||||
expect.objectContaining({ pluginId: 'barchart' }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'barchart',
|
||||
options: expect.objectContaining({ orientation: VizOrientation.Horizontal }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'gauge' }),
|
||||
expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }),
|
||||
expect.objectContaining({ pluginId: 'stat' }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'stat',
|
||||
options: expect.objectContaining({ colorMode: BigValueColorMode.Background }),
|
||||
}),
|
||||
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'table' }),
|
||||
expect.objectContaining({ pluginId: 'histogram' }),
|
||||
expect(ctx.names()).toEqual([
|
||||
SuggestionName.BarChart,
|
||||
SuggestionName.BarChartHorizontal,
|
||||
SuggestionName.Gauge,
|
||||
SuggestionName.GaugeNoThresholds,
|
||||
SuggestionName.Stat,
|
||||
SuggestionName.StatColoredBackground,
|
||||
SuggestionName.PieChart,
|
||||
SuggestionName.PieChartDonut,
|
||||
SuggestionName.BarGaugeBasic,
|
||||
SuggestionName.BarGaugeLCD,
|
||||
SuggestionName.Table,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Scalar panels should contain raw values', () => {
|
||||
for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) {
|
||||
expect(suggestion).toEqual(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({ reduceOptions: expect.objectContaining({ values: true, calcs: [] }) }),
|
||||
})
|
||||
);
|
||||
it('Stat/Gauge/BarGauge/PieChart panels to have reduceOptions.values enabled', () => {
|
||||
for (const suggestion of ctx.suggestions) {
|
||||
if (suggestion.options?.reduceOptions && !suggestion.options?.reduceOptions?.values) {
|
||||
throw new Error(`Suggestion ${suggestion.name} reduce.values set to false when it should be true`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -326,48 +271,22 @@ scenario('Single frame with string and 2 number field', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([
|
||||
expect.objectContaining({ pluginId: 'barchart' }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'barchart',
|
||||
options: expect.objectContaining({ stacking: StackingMode.Normal }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'barchart',
|
||||
options: expect.objectContaining({ stacking: StackingMode.Percent }),
|
||||
}),
|
||||
|
||||
expect.objectContaining({
|
||||
pluginId: 'barchart',
|
||||
options: expect.objectContaining({ orientation: VizOrientation.Horizontal }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'barchart',
|
||||
options: expect.objectContaining({ orientation: VizOrientation.Horizontal, stacking: StackingMode.Normal }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'barchart',
|
||||
options: expect.objectContaining({ orientation: VizOrientation.Horizontal, stacking: StackingMode.Percent }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'gauge' }),
|
||||
expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }),
|
||||
expect.objectContaining({ pluginId: 'stat' }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'stat',
|
||||
options: expect.objectContaining({ colorMode: BigValueColorMode.Background }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'piechart' }),
|
||||
expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }),
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
pluginId: 'bargauge',
|
||||
options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }),
|
||||
}),
|
||||
expect.objectContaining({ pluginId: 'table' }),
|
||||
expect.objectContaining({ pluginId: 'histogram' }),
|
||||
expect(ctx.names()).toEqual([
|
||||
SuggestionName.BarChart,
|
||||
SuggestionName.BarChartStacked,
|
||||
SuggestionName.BarChartStackedPercent,
|
||||
SuggestionName.BarChartHorizontal,
|
||||
SuggestionName.BarChartHorizontalStacked,
|
||||
SuggestionName.BarChartHorizontalStackedPercent,
|
||||
SuggestionName.Gauge,
|
||||
SuggestionName.GaugeNoThresholds,
|
||||
SuggestionName.Stat,
|
||||
SuggestionName.StatColoredBackground,
|
||||
SuggestionName.PieChart,
|
||||
SuggestionName.PieChartDonut,
|
||||
SuggestionName.BarGaugeBasic,
|
||||
SuggestionName.BarGaugeLCD,
|
||||
SuggestionName.Table,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -380,14 +299,11 @@ scenario('Single frame with only string field', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([
|
||||
expect.objectContaining({ pluginId: 'stat' }),
|
||||
expect.objectContaining({ pluginId: 'table' }),
|
||||
]);
|
||||
expect(ctx.names()).toEqual([SuggestionName.Stat, SuggestionName.Table]);
|
||||
});
|
||||
|
||||
it('Stat panels have reduceOptions.fields set to show all fields', () => {
|
||||
for (const suggestion of ctx.suggestions.filter((s) => s.pluginId === 'stat')) {
|
||||
for (const suggestion of ctx.suggestions) {
|
||||
if (suggestion.options?.reduceOptions) {
|
||||
expect(suggestion.options.reduceOptions.fields).toBe('/.*/');
|
||||
}
|
||||
@@ -417,10 +333,7 @@ scenario('Given default loki logs data', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return correct suggestions', () => {
|
||||
expect(ctx.suggestions).toEqual([
|
||||
expect.objectContaining({ pluginId: 'logs' }),
|
||||
expect.objectContaining({ pluginId: 'table' }),
|
||||
]);
|
||||
expect(ctx.names()).toEqual([SuggestionName.Logs, SuggestionName.Table]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -443,7 +356,7 @@ scenario('Given a preferredVisualisationType', (ctx) => {
|
||||
]);
|
||||
|
||||
it('should return the preferred visualization first', () => {
|
||||
expect(ctx.suggestions[0]).toEqual(expect.objectContaining({ pluginId: 'table' }));
|
||||
expect(ctx.names()[0]).toEqual(SuggestionName.Table);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
VisualizationSuggestionsBuilder,
|
||||
PanelModel,
|
||||
VisualizationSuggestionScore,
|
||||
PreferredVisualisationType,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin';
|
||||
@@ -24,26 +23,8 @@ export const panelsToCheckFirst = [
|
||||
'flamegraph',
|
||||
'traces',
|
||||
'nodeGraph',
|
||||
'heatmap',
|
||||
'histogram',
|
||||
'geomap',
|
||||
];
|
||||
|
||||
/**
|
||||
* some of the PreferredVisualisationTypes do not match the panel plugin ids, so we have to map them. d'oh.
|
||||
*/
|
||||
const PLUGIN_ID_TO_PREFERRED_VIZ_TYPE: Record<string, PreferredVisualisationType> = {
|
||||
traces: 'trace',
|
||||
timeseries: 'graph',
|
||||
table: 'table',
|
||||
logs: 'logs',
|
||||
nodeGraph: 'nodeGraph',
|
||||
flamegraph: 'flamegraph',
|
||||
};
|
||||
const mapPreferredVisualisationTypeToPlugin = (type: string): PreferredVisualisationType | undefined => {
|
||||
return PLUGIN_ID_TO_PREFERRED_VIZ_TYPE[type];
|
||||
};
|
||||
|
||||
export async function getAllSuggestions(
|
||||
data?: PanelData,
|
||||
panel?: PanelModel
|
||||
@@ -80,13 +61,13 @@ export async function getAllSuggestions(
|
||||
}
|
||||
|
||||
return list.sort((a, b) => {
|
||||
const mappedA = mapPreferredVisualisationTypeToPlugin(a.pluginId);
|
||||
if (mappedA && builder.dataSummary.hasPreferredVisualisationType(mappedA)) {
|
||||
return -1;
|
||||
}
|
||||
const mappedB = mapPreferredVisualisationTypeToPlugin(a.pluginId);
|
||||
if (mappedB && builder.dataSummary.hasPreferredVisualisationType(mappedB)) {
|
||||
return 1;
|
||||
if (builder.dataSummary.preferredVisualisationType) {
|
||||
if (a.pluginId === builder.dataSummary.preferredVisualisationType) {
|
||||
return -1;
|
||||
}
|
||||
if (b.pluginId === builder.dataSummary.preferredVisualisationType) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return (b.score ?? VisualizationSuggestionScore.OK) - (a.score ?? VisualizationSuggestionScore.OK);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
DataFrameType,
|
||||
PanelData,
|
||||
PanelDataSummary,
|
||||
VisualizationSuggestion,
|
||||
VisualizationSuggestionScore,
|
||||
} from '@grafana/data';
|
||||
import { ReduceDataOptions } from '@grafana/schema';
|
||||
import { PanelData, PanelDataSummary } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -17,42 +10,6 @@ export function showDefaultSuggestion(fn: (panelDataSummary: PanelDataSummary) =
|
||||
return (panelDataSummary: PanelDataSummary) => (fn(panelDataSummary) ? [{}] : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* for panel plugins which render "scalar" data (stat, gauge, etc), this helper provides default reduce options
|
||||
* depending on whether deaggregation is likely needed.
|
||||
* @param suggestion the suggestion to modify
|
||||
* @param panelDataSummary the panel data summary to use for scoring
|
||||
* @param shouldUseRawValues if true, reduceOptions will be set to use raw values,
|
||||
* otherwise a calcs will be used with the default value of `lastNotNull`.
|
||||
*/
|
||||
export function defaultNumericVizOptions(
|
||||
suggestion: VisualizationSuggestion<{ reduceOptions?: ReduceDataOptions }>,
|
||||
panelDataSummary: PanelDataSummary,
|
||||
shouldUseRawValues: boolean
|
||||
): VisualizationSuggestion {
|
||||
suggestion.score =
|
||||
(suggestion.score ??
|
||||
(panelDataSummary.hasDataFrameType(DataFrameType.NumericLong) ||
|
||||
panelDataSummary.hasDataFrameType(DataFrameType.NumericWide) ||
|
||||
panelDataSummary.hasDataFrameType(DataFrameType.NumericMulti)))
|
||||
? VisualizationSuggestionScore.Good
|
||||
: VisualizationSuggestionScore.OK;
|
||||
suggestion.options = suggestion.options ?? {};
|
||||
suggestion.options.reduceOptions =
|
||||
suggestion.options.reduceOptions ??
|
||||
(shouldUseRawValues
|
||||
? {
|
||||
values: true,
|
||||
calcs: [],
|
||||
}
|
||||
: {
|
||||
values: false,
|
||||
calcs: ['lastNotNull'],
|
||||
});
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Checks if the panel has data
|
||||
|
||||
@@ -1000,8 +1000,7 @@ describe('Plugin Extensions / Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Fix tests suites below so they work with new plugins structure
|
||||
describe.skip('getAppPluginConfigs()', () => {
|
||||
describe('getAppPluginConfigs()', () => {
|
||||
const originalApps = config.apps;
|
||||
const genereicAppPluginConfig = {
|
||||
path: '',
|
||||
@@ -1074,13 +1073,13 @@ describe('Plugin Extensions / Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getAppPluginIdFromExposedComponentId()', () => {
|
||||
describe('getAppPluginIdFromExposedComponentId()', () => {
|
||||
test('should return the app plugin id from an extension point id', () => {
|
||||
expect(getAppPluginIdFromExposedComponentId('myorg-extensions-app/component/v1')).toBe('myorg-extensions-app');
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getExtensionPointPluginDependencies()', () => {
|
||||
describe('getExtensionPointPluginDependencies()', () => {
|
||||
const originalApps = config.apps;
|
||||
const genereicAppPluginConfig = {
|
||||
path: '',
|
||||
@@ -1294,7 +1293,7 @@ describe('Plugin Extensions / Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getExposedComponentPluginDependencies()', () => {
|
||||
describe('getExposedComponentPluginDependencies()', () => {
|
||||
const originalApps = config.apps;
|
||||
const genereicAppPluginConfig = {
|
||||
path: '',
|
||||
@@ -1440,7 +1439,7 @@ describe('Plugin Extensions / Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getAppPluginDependencies()', () => {
|
||||
describe('getAppPluginDependencies()', () => {
|
||||
const originalApps = config.apps;
|
||||
const genereicAppPluginConfig = {
|
||||
path: '',
|
||||
@@ -1530,7 +1529,7 @@ describe('Plugin Extensions / Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('getExtensionPointPluginMeta()', () => {
|
||||
describe('getExtensionPointPluginMeta()', () => {
|
||||
const originalApps = config.apps;
|
||||
const mockExtensionPointId = 'test-extension-point';
|
||||
const mockApp1: AppPluginConfig = {
|
||||
|
||||
@@ -15,12 +15,10 @@ import {
|
||||
urlUtil,
|
||||
PluginExtensionPoints,
|
||||
ExtensionInfo,
|
||||
type PluginMetasSpec,
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction, config } from '@grafana/runtime';
|
||||
import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime';
|
||||
import { Modal } from '@grafana/ui';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
import {
|
||||
CloseExtensionSidebarEvent,
|
||||
@@ -607,7 +605,7 @@ export function getLinkExtensionPathWithTracking(pluginId: string, path: string,
|
||||
export const isGrafanaDevMode = () => config.buildInfo.env === 'development';
|
||||
|
||||
export const getAppPluginConfigs = (pluginIds: string[] = []) =>
|
||||
Object.values(getConfig().plugins.apps).filter((app) => pluginIds.includes(app.pluginJson.id));
|
||||
Object.values(config.apps).filter((app) => pluginIds.includes(app.id));
|
||||
|
||||
export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => {
|
||||
return exposedComponentId.split('/')[0];
|
||||
@@ -617,13 +615,13 @@ export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string)
|
||||
// (These plugins are necessary to be loaded to use the extension point.)
|
||||
// (The function also returns the plugin ids that the plugins - that extend the extension point - depend on.)
|
||||
export const getExtensionPointPluginDependencies = (extensionPointId: string): string[] => {
|
||||
return Object.values(getConfig().plugins.apps)
|
||||
return Object.values(config.apps)
|
||||
.filter(
|
||||
(app) =>
|
||||
app.pluginJson.extensions?.addedLinks?.some((link) => link.targets.includes(extensionPointId)) ||
|
||||
app.pluginJson.extensions?.addedComponents?.some((component) => component.targets.includes(extensionPointId))
|
||||
app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) ||
|
||||
app.extensions.addedComponents.some((component) => component.targets.includes(extensionPointId))
|
||||
)
|
||||
.map((app) => app.pluginJson.id)
|
||||
.map((app) => app.id)
|
||||
.reduce((acc: string[], id: string) => {
|
||||
return [...acc, id, ...getAppPluginDependencies(id)];
|
||||
}, []);
|
||||
@@ -646,26 +644,22 @@ export const getExtensionPointPluginMeta = (extensionPointId: string): Extension
|
||||
return new Map(
|
||||
getExtensionPointPluginDependencies(extensionPointId)
|
||||
.map((pluginId) => {
|
||||
const app = getConfig().plugins.apps[pluginId];
|
||||
const app = config.apps[pluginId];
|
||||
// if the plugin does not exist or does not expose any components or links to the extension point, return undefined
|
||||
if (
|
||||
!app ||
|
||||
(!app.pluginJson.extensions?.addedComponents?.some((component) =>
|
||||
component.targets.includes(extensionPointId)
|
||||
) &&
|
||||
!app.pluginJson.extensions?.addedLinks?.some((link) => link.targets.includes(extensionPointId)))
|
||||
(!app.extensions.addedComponents.some((component) => component.targets.includes(extensionPointId)) &&
|
||||
!app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return [
|
||||
pluginId,
|
||||
{
|
||||
addedComponents:
|
||||
app.pluginJson.extensions?.addedComponents?.filter((component) =>
|
||||
component.targets.includes(extensionPointId)
|
||||
) || [],
|
||||
addedLinks:
|
||||
app.pluginJson.extensions?.addedLinks?.filter((link) => link.targets.includes(extensionPointId)) || [],
|
||||
addedComponents: app.extensions.addedComponents.filter((component) =>
|
||||
component.targets.includes(extensionPointId)
|
||||
),
|
||||
addedLinks: app.extensions.addedLinks.filter((link) => link.targets.includes(extensionPointId)),
|
||||
},
|
||||
] as const;
|
||||
})
|
||||
@@ -687,7 +681,7 @@ export const getExposedComponentPluginDependencies = (exposedComponentId: string
|
||||
// metadata field. (For example the plugins that expose components that the app depends on.)
|
||||
// Heads up! This is a recursive function.
|
||||
export const getAppPluginDependencies = (pluginId: string, visited: string[] = []): string[] => {
|
||||
if (!getConfig().plugins.apps[pluginId]) {
|
||||
if (!config.apps[pluginId]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -696,18 +690,17 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [
|
||||
return [];
|
||||
}
|
||||
|
||||
const pluginIdDependencies =
|
||||
getConfig().plugins.apps[pluginId]?.pluginJson.dependencies?.extensions?.exposedComponents?.map(
|
||||
getAppPluginIdFromExposedComponentId
|
||||
) || [];
|
||||
const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map(
|
||||
getAppPluginIdFromExposedComponentId
|
||||
);
|
||||
|
||||
return (
|
||||
pluginIdDependencies
|
||||
?.reduce((acc, _pluginId) => {
|
||||
.reduce((acc, _pluginId) => {
|
||||
return [...acc, ...getAppPluginDependencies(_pluginId, [...visited, pluginId])];
|
||||
}, pluginIdDependencies)
|
||||
// We don't want the plugin to "depend on itself"
|
||||
.filter((id) => id !== pluginId) || []
|
||||
.filter((id) => id !== pluginId)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -718,17 +711,17 @@ export const getAppPluginsToAwait = () => {
|
||||
'cloud-home-app',
|
||||
];
|
||||
|
||||
return Object.values(getConfig().plugins.apps).filter((app) => pluginIds.includes(app.pluginJson.id));
|
||||
return Object.values(config.apps).filter((app) => pluginIds.includes(app.id));
|
||||
};
|
||||
|
||||
// Returns a list of app plugins that has to be preloaded in parallel with the core Grafana initialization.
|
||||
export const getAppPluginsToPreload = () => {
|
||||
// The DashboardPanelMenu extension point is using the `getPluginExtensions()` API in scenes at the moment, which means that it cannot yet benefit from dynamic plugin loading.
|
||||
const dashboardPanelMenuPluginIds = getExtensionPointPluginDependencies(PluginExtensionPoints.DashboardPanelMenu);
|
||||
const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.pluginJson.id);
|
||||
const isNotAwaited = (app: PluginMetasSpec) => !awaitedPluginIds.includes(app.pluginJson.id);
|
||||
const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.id);
|
||||
const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(app.id);
|
||||
|
||||
return Object.values(getConfig().plugins.apps).filter((app) => {
|
||||
return isNotAwaited(app) && (app.pluginJson.preload || dashboardPanelMenuPluginIds.includes(app.pluginJson.id));
|
||||
return Object.values(config.apps).filter((app) => {
|
||||
return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
type PluginExtensions,
|
||||
AppPlugin,
|
||||
OrgRole,
|
||||
type PluginMetasSpec,
|
||||
PluginSignatureStatus,
|
||||
} from '@grafana/data';
|
||||
import { ContextSrv, setContextSrv } from 'app/core/services/context_srv';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
@@ -29,74 +27,26 @@ jest.mock('./importer/pluginImporter', () => ({
|
||||
const getPluginSettingsMock = jest.mocked(getPluginSettings);
|
||||
const importAppPluginMock = jest.mocked(pluginImporter.importApp);
|
||||
|
||||
const createMockAppPluginConfig = (overrides: Partial<AppPluginConfig> = {}): PluginMetasSpec => {
|
||||
const app: AppPluginConfig = {
|
||||
id: 'test-plugin',
|
||||
path: '/path/to/plugin',
|
||||
version: '1.0.0',
|
||||
preload: true,
|
||||
angular: { detected: false, hideDeprecation: false } as AngularMeta,
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: { exposedComponents: [] },
|
||||
} as PluginDependencies,
|
||||
extensions: {
|
||||
addedComponents: [],
|
||||
addedLinks: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
} as PluginExtensions,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return createMockSpec({ path: app.path, loadingStrategy: app.loadingStrategy }, { id: app.id });
|
||||
};
|
||||
|
||||
const createMockSpec = (
|
||||
module: Partial<PluginMetasSpec['module']> = {},
|
||||
pluginJson: Partial<PluginMetasSpec['pluginJson']> = {}
|
||||
): PluginMetasSpec => ({
|
||||
module: {
|
||||
path: '/path/to/plugin',
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
...module,
|
||||
},
|
||||
pluginJson: {
|
||||
id: 'test-plugin',
|
||||
name: 'Test Plugin',
|
||||
type: 'app',
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
keywords: [],
|
||||
logos: { large: '', small: '' },
|
||||
updated: '',
|
||||
},
|
||||
preload: true,
|
||||
dependencies: {
|
||||
grafanaVersion: '*',
|
||||
grafanaDependency: '',
|
||||
plugins: [],
|
||||
extensions: { exposedComponents: [] },
|
||||
},
|
||||
extensions: {
|
||||
addedComponents: [],
|
||||
addedLinks: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
},
|
||||
...pluginJson,
|
||||
},
|
||||
angular: {
|
||||
detected: false,
|
||||
},
|
||||
baseURL: '/path/to/plugin',
|
||||
signature: {
|
||||
status: PluginSignatureStatus.missing,
|
||||
},
|
||||
const createMockAppPluginConfig = (overrides: Partial<AppPluginConfig> = {}): AppPluginConfig => ({
|
||||
id: 'test-plugin',
|
||||
path: '/path/to/plugin',
|
||||
version: '1.0.0',
|
||||
preload: true,
|
||||
angular: { detected: false, hideDeprecation: false } as AngularMeta,
|
||||
loadingStrategy: PluginLoadingStrategy.fetch,
|
||||
dependencies: {
|
||||
grafanaVersion: '*',
|
||||
plugins: [],
|
||||
extensions: { exposedComponents: [] },
|
||||
} as PluginDependencies,
|
||||
extensions: {
|
||||
addedComponents: [],
|
||||
addedLinks: [],
|
||||
addedFunctions: [],
|
||||
exposedComponents: [],
|
||||
extensionPoints: [],
|
||||
} as PluginExtensions,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockPluginMeta = (overrides: Partial<PluginMeta> = {}): PluginMeta => ({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type {
|
||||
AppPluginConfig,
|
||||
PluginExtensionAddedLinkConfig,
|
||||
PluginExtensionExposedComponentConfig,
|
||||
PluginExtensionAddedComponentConfig,
|
||||
PluginMetasSpec,
|
||||
} from '@grafana/data';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
@@ -23,32 +23,29 @@ export const clearPreloadedPluginsCache = () => {
|
||||
preloadPromises.clear();
|
||||
};
|
||||
|
||||
export async function preloadPlugins(apps: PluginMetasSpec[] = []) {
|
||||
export async function preloadPlugins(apps: AppPluginConfig[] = []) {
|
||||
// Create preload promises for each app, reusing existing promises if already loading
|
||||
const promises = apps.map((app) => {
|
||||
if (!preloadPromises.has(app.pluginJson.id)) {
|
||||
preloadPromises.set(app.pluginJson.id, preload(app));
|
||||
if (!preloadPromises.has(app.id)) {
|
||||
preloadPromises.set(app.id, preload(app));
|
||||
}
|
||||
return preloadPromises.get(app.pluginJson.id)!;
|
||||
return preloadPromises.get(app.id)!;
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async function preload(config: PluginMetasSpec): Promise<void> {
|
||||
async function preload(config: AppPluginConfig): Promise<void> {
|
||||
const showErrorAlert = contextSrv.user.orgRole !== '';
|
||||
|
||||
try {
|
||||
const meta = await getPluginSettings(config.pluginJson.id, { showErrorAlert });
|
||||
const meta = await getPluginSettings(config.id, { showErrorAlert });
|
||||
await pluginImporter.importApp(meta);
|
||||
} catch (error) {
|
||||
if (!showErrorAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[Plugins] Failed to preload plugin: ${config.module.path} (version: ${config.pluginJson.info.version})`,
|
||||
error
|
||||
);
|
||||
console.error(`[Plugins] Failed to preload plugin: ${config.path} (version: ${config.version})`, error);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -2,13 +2,12 @@ import { ReactNode } from 'react';
|
||||
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Badge, Card, LinkButton, Stack, Text, TextLink } from '@grafana/ui';
|
||||
import { Stack, Text, TextLink, Icon, Card, LinkButton, Badge } from '@grafana/ui';
|
||||
import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1';
|
||||
|
||||
import { RepoIcon } from '../Shared/RepoIcon';
|
||||
import { StatusBadge } from '../Shared/StatusBadge';
|
||||
import { PROVISIONING_URL } from '../constants';
|
||||
import { getRepoHrefForProvider } from '../utils/git';
|
||||
import { getIsReadOnlyWorkflows } from '../utils/repository';
|
||||
|
||||
import { SyncRepository } from './SyncRepository';
|
||||
@@ -17,7 +16,7 @@ interface Props {
|
||||
repository: Repository;
|
||||
}
|
||||
|
||||
export function RepositoryListItem({ repository }: Props) {
|
||||
export function RepositoryCard({ repository }: Props) {
|
||||
const isReadOnlyRepo = getIsReadOnlyWorkflows(repository.spec?.workflows);
|
||||
const { metadata, spec, status } = repository;
|
||||
const name = metadata?.name ?? '';
|
||||
@@ -28,13 +27,24 @@ export function RepositoryListItem({ repository }: Props) {
|
||||
if (spec?.type === 'github') {
|
||||
const { url = '', branch } = spec.github ?? {};
|
||||
const branchUrl = branch ? `${url}/tree/${branch}` : url;
|
||||
const href = getRepoHrefForProvider(spec) || branchUrl;
|
||||
|
||||
meta.push(
|
||||
<TextLink key="link" external href={href}>
|
||||
{href.split('/').slice(3).join('/')}
|
||||
<TextLink key="link" external href={branchUrl}>
|
||||
{branchUrl}
|
||||
</TextLink>
|
||||
);
|
||||
|
||||
if (status?.webhook?.id) {
|
||||
const webhookUrl = `${url}/settings/hooks/${status.webhook.id}`;
|
||||
meta.push(
|
||||
<Stack gap={1} direction="row" alignItems="center">
|
||||
<TextLink key="webhook" href={webhookUrl}>
|
||||
<Trans i18nKey="provisioning.repository-card.get-repository-meta.webhook">Webhook</Trans>
|
||||
</TextLink>
|
||||
<Icon name="check" className="text-success" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
} else if (spec?.type === 'local') {
|
||||
meta.push(
|
||||
<Text variant="bodySmall" key="path">
|
||||
@@ -4,7 +4,7 @@ import { Trans } from '@grafana/i18n';
|
||||
import { LinkButton, Stack, Text, TextLink } from '@grafana/ui';
|
||||
import { useGetRepositoryQuery } from 'app/api/clients/provisioning/v0alpha1';
|
||||
|
||||
import { getRepoHrefForProvider } from '../utils/git';
|
||||
import { getRepoHref } from '../utils/git';
|
||||
|
||||
type RepositoryLinkProps = {
|
||||
name?: string;
|
||||
@@ -19,7 +19,7 @@ export function RepositoryLink({ name, jobType }: RepositoryLinkProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repoHref = getRepoHrefForProvider(repo.spec);
|
||||
const repoHref = getRepoHref(repo.spec?.github);
|
||||
|
||||
if (jobType === 'sync') {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { t, Trans } from '@grafana/i18n';
|
||||
import { Alert, Box, EmptyState, FilterInput, Icon, Stack, TextLink } from '@grafana/ui';
|
||||
import { Repository } from 'app/api/clients/provisioning/v0alpha1';
|
||||
|
||||
import { RepositoryListItem } from '../Repository/RepositoryListItem';
|
||||
import { RepositoryCard } from '../Repository/RepositoryCard';
|
||||
import { useResourceStats } from '../Wizard/hooks/useResourceStats';
|
||||
import { UPGRADE_URL } from '../constants';
|
||||
import { useIsProvisionedInstance } from '../hooks/useIsProvisionedInstance';
|
||||
@@ -89,7 +89,7 @@ export function RepositoryList({ items }: Props) {
|
||||
)}
|
||||
<Stack direction={'column'} gap={2}>
|
||||
{filteredItems.length ? (
|
||||
filteredItems.map((item) => <RepositoryListItem key={item.metadata?.name} repository={item} />)
|
||||
filteredItems.map((item) => <RepositoryCard key={item.metadata?.name} repository={item} />)
|
||||
) : (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
|
||||
@@ -107,14 +107,9 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl, b
|
||||
{/* when repo type is not local, we show branch information */}
|
||||
{showBranchInfo(repoType, branchInfo) && (
|
||||
<Box marginTop={1}>
|
||||
<Trans i18nKey="provisioned-resource-preview-banner.preview-banner.branch-text">branch:</Trans>{' '}
|
||||
<TextLink href={getBranchUrl(repoBaseUrl!, targetBranch!, repoType)} external>
|
||||
{targetBranch}
|
||||
</TextLink>{' '}
|
||||
{'\u2192'}{' '}
|
||||
<TextLink href={getBranchUrl(repoBaseUrl!, configuredBranch!, repoType)} external>
|
||||
{configuredBranch}
|
||||
</TextLink>
|
||||
<Trans i18nKey="provisioned-resource-preview-banner.preview-banner.branch-text">branch: </Trans>
|
||||
<TextLink href={getBranchUrl(repoBaseUrl!, targetBranch!, repoType)}>{targetBranch}</TextLink> {'\u2192'}{' '}
|
||||
<TextLink href={getBranchUrl(repoBaseUrl!, configuredBranch!, repoType)}>{configuredBranch}</TextLink>
|
||||
</Box>
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user