diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index e2084f9b8d8..edcf18ea3e3 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -24,6 +24,7 @@ require ( require ( cel.dev/expr v0.25.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + 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 @@ -35,16 +36,21 @@ 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-20250403215159-8d39553ac7cf // 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-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -94,6 +100,7 @@ require ( github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect github.com/grafana/grafana/pkg/apimachinery v0.0.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect + github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grafana/sqlds/v4 v4.2.7 // indirect @@ -142,11 +149,15 @@ 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 @@ -165,6 +176,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect github.com/tjhop/slog-gokit v0.1.5 // indirect github.com/woodsbury/decimal128 v1.4.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -179,6 +191,7 @@ require ( go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect @@ -186,6 +199,8 @@ require ( go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index dcbf9d3a129..c5fbc7a39a5 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -4,9 +4,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT 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/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +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= @@ -38,12 +42,18 @@ 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-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I= github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= @@ -60,6 +70,8 @@ github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wX 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= @@ -69,6 +81,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= @@ -219,6 +233,8 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws= github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E= +github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= +github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U= @@ -366,6 +382,8 @@ 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= @@ -380,6 +398,12 @@ 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= @@ -465,6 +489,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o 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/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= @@ -513,6 +541,8 @@ go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bn go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= @@ -532,8 +562,12 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6 go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +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.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= diff --git a/apps/plugins/kinds/meta.cue b/apps/plugins/kinds/meta.cue index 34a7b9aaf12..d29308e2ca2 100644 --- a/apps/plugins/kinds/meta.cue +++ b/apps/plugins/kinds/meta.cue @@ -5,7 +5,25 @@ metaV0Alpha1: { scope: "Namespaced" schema: { spec: { - pluginJSON: #JSONData, + pluginJson: #JSONData + class: "core" | "external" + 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] } } } diff --git a/apps/plugins/kinds/plugin.cue b/apps/plugins/kinds/plugin.cue index dc16a3f6de4..854448a5b72 100644 --- a/apps/plugins/kinds/plugin.cue +++ b/apps/plugins/kinds/plugin.cue @@ -9,7 +9,6 @@ pluginV0Alpha1: { id: string version: string url?: string - class: "core" | "external" } } } diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go index 9598da22ef7..f3671a040db 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/meta_spec_gen.go @@ -208,13 +208,21 @@ func NewMetaExtensions() *MetaExtensions { // +k8s:openapi-gen=true type MetaSpec struct { - PluginJSON MetaJSONData `json:"pluginJSON"` + PluginJson MetaJSONData `json:"pluginJson"` + Class MetaSpecClass `json:"class"` + Module *MetaV0alpha1SpecModule `json:"module,omitempty"` + BaseURL *string `json:"baseURL,omitempty"` + Signature *MetaV0alpha1SpecSignature `json:"signature,omitempty"` + Angular *MetaV0alpha1SpecAngular `json:"angular,omitempty"` + Translations map[string]string `json:"translations,omitempty"` + // +listType=atomic + Children []string `json:"children,omitempty"` } // NewMetaSpec creates a new MetaSpec object. func NewMetaSpec() *MetaSpec { return &MetaSpec{ - PluginJSON: *NewMetaJSONData(), + PluginJson: *NewMetaJSONData(), } } @@ -412,6 +420,40 @@ func NewMetaV0alpha1ExtensionsExtensionPoints() *MetaV0alpha1ExtensionsExtension return &MetaV0alpha1ExtensionsExtensionPoints{} } +// +k8s:openapi-gen=true +type MetaV0alpha1SpecModule struct { + Path string `json:"path"` + Hash *string `json:"hash,omitempty"` + LoadingStrategy *MetaV0alpha1SpecModuleLoadingStrategy `json:"loadingStrategy,omitempty"` +} + +// NewMetaV0alpha1SpecModule creates a new MetaV0alpha1SpecModule object. +func NewMetaV0alpha1SpecModule() *MetaV0alpha1SpecModule { + return &MetaV0alpha1SpecModule{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecSignature struct { + Status MetaV0alpha1SpecSignatureStatus `json:"status"` + Type *MetaV0alpha1SpecSignatureType `json:"type,omitempty"` + Org *string `json:"org,omitempty"` +} + +// NewMetaV0alpha1SpecSignature creates a new MetaV0alpha1SpecSignature object. +func NewMetaV0alpha1SpecSignature() *MetaV0alpha1SpecSignature { + return &MetaV0alpha1SpecSignature{} +} + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecAngular struct { + Detected bool `json:"detected"` +} + +// NewMetaV0alpha1SpecAngular creates a new MetaV0alpha1SpecAngular object. +func NewMetaV0alpha1SpecAngular() *MetaV0alpha1SpecAngular { + return &MetaV0alpha1SpecAngular{} +} + // +k8s:openapi-gen=true type MetaJSONDataType string @@ -464,6 +506,14 @@ const ( MetaIncludeRoleViewer MetaIncludeRole = "Viewer" ) +// +k8s:openapi-gen=true +type MetaSpecClass string + +const ( + MetaSpecClassCore MetaSpecClass = "core" + MetaSpecClassExternal MetaSpecClass = "external" +) + // +k8s:openapi-gen=true type MetaV0alpha1DependenciesPluginsType string @@ -472,3 +522,33 @@ const ( MetaV0alpha1DependenciesPluginsTypeDatasource MetaV0alpha1DependenciesPluginsType = "datasource" MetaV0alpha1DependenciesPluginsTypePanel MetaV0alpha1DependenciesPluginsType = "panel" ) + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecModuleLoadingStrategy string + +const ( + MetaV0alpha1SpecModuleLoadingStrategyFetch MetaV0alpha1SpecModuleLoadingStrategy = "fetch" + MetaV0alpha1SpecModuleLoadingStrategyScript MetaV0alpha1SpecModuleLoadingStrategy = "script" +) + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecSignatureStatus string + +const ( + MetaV0alpha1SpecSignatureStatusInternal MetaV0alpha1SpecSignatureStatus = "internal" + MetaV0alpha1SpecSignatureStatusValid MetaV0alpha1SpecSignatureStatus = "valid" + MetaV0alpha1SpecSignatureStatusInvalid MetaV0alpha1SpecSignatureStatus = "invalid" + MetaV0alpha1SpecSignatureStatusModified MetaV0alpha1SpecSignatureStatus = "modified" + MetaV0alpha1SpecSignatureStatusUnsigned MetaV0alpha1SpecSignatureStatus = "unsigned" +) + +// +k8s:openapi-gen=true +type MetaV0alpha1SpecSignatureType string + +const ( + MetaV0alpha1SpecSignatureTypeGrafana MetaV0alpha1SpecSignatureType = "grafana" + MetaV0alpha1SpecSignatureTypeCommercial MetaV0alpha1SpecSignatureType = "commercial" + MetaV0alpha1SpecSignatureTypeCommunity MetaV0alpha1SpecSignatureType = "community" + MetaV0alpha1SpecSignatureTypePrivate MetaV0alpha1SpecSignatureType = "private" + MetaV0alpha1SpecSignatureTypePrivateGlob MetaV0alpha1SpecSignatureType = "private-glob" +) diff --git a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go index 9ae14bcbdc5..51f5440eb92 100644 --- a/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go +++ b/apps/plugins/pkg/apis/plugins/v0alpha1/plugin_spec_gen.go @@ -4,21 +4,12 @@ package v0alpha1 // +k8s:openapi-gen=true type PluginSpec struct { - Id string `json:"id"` - Version string `json:"version"` - Url *string `json:"url,omitempty"` - Class PluginSpecClass `json:"class"` + Id string `json:"id"` + Version string `json:"version"` + Url *string `json:"url,omitempty"` } // NewPluginSpec creates a new PluginSpec object. func NewPluginSpec() *PluginSpec { return &PluginSpec{} } - -// +k8s:openapi-gen=true -type PluginSpecClass string - -const ( - PluginSpecClassCore PluginSpecClass = "core" - PluginSpecClassExternal PluginSpecClass = "external" -) diff --git a/apps/plugins/pkg/apis/plugins_manifest.go b/apps/plugins/pkg/apis/plugins_manifest.go index 1a351eb2baf..7b87dcf0ea7 100644 --- a/apps/plugins/pkg/apis/plugins_manifest.go +++ b/apps/plugins/pkg/apis/plugins_manifest.go @@ -20,10 +20,10 @@ import ( ) var ( - rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"class":{"enum":["core","external"],"type":"string"},"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaPluginv0alpha1 = []byte(`{"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"Plugin":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"id":{"type":"string"},"url":{"type":"string"},"version":{"type":"string"}},"required":["id","version"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaPluginv0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaPluginv0alpha1, &versionSchemaPluginv0alpha1) - rawSchemaMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"suggestions":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"Meta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"pluginJSON":{"$ref":"#/components/schemas/JSONData"}},"required":["pluginJSON"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) + rawSchemaMetav0alpha1 = []byte(`{"Dependencies":{"additionalProperties":false,"properties":{"extensions":{"additionalProperties":false,"properties":{"exposedComponents":{"description":"+listType=set","items":{"type":"string"},"type":"array"}},"type":"object"},"grafanaDependency":{"description":"Required field","type":"string"},"grafanaVersion":{"description":"Optional fields","type":"string"},"plugins":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"enum":["app","datasource","panel"],"type":"string"}},"required":["id","type","name"],"type":"object"},"type":"array"}},"required":["grafanaDependency"],"type":"object"},"EnterpriseFeatures":{"additionalProperties":false,"properties":{"healthDiagnosticsErrors":{"default":false,"description":"Allow additional properties","type":"boolean"}},"type":"object"},"Extensions":{"additionalProperties":false,"properties":{"addedComponents":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"addedLinks":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"targets":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["targets","title"],"type":"object"},"type":"array"},"exposedComponents":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"},"extensionPoints":{"description":"+listType=set\n+listMapKey=id","items":{"additionalProperties":false,"properties":{"description":{"type":"string"},"id":{"type":"string"},"title":{"type":"string"}},"required":["id"],"type":"object"},"type":"array"}},"type":"object"},"IAM":{"additionalProperties":false,"properties":{"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"Include":{"additionalProperties":false,"properties":{"action":{"type":"string"},"addToNav":{"type":"boolean"},"component":{"type":"string"},"defaultNav":{"type":"boolean"},"icon":{"type":"string"},"name":{"type":"string"},"path":{"type":"string"},"role":{"enum":["Admin","Editor","Viewer"],"type":"string"},"type":{"enum":["dashboard","page","panel","datasource"],"type":"string"},"uid":{"type":"string"}},"type":"object"},"Info":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"description":"Optional fields","properties":{"email":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"description":{"type":"string"},"keywords":{"description":"Required fields\n+listType=set","items":{"type":"string"},"type":"array"},"links":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"url":{"type":"string"}},"type":"object"},"type":"array"},"logos":{"additionalProperties":false,"properties":{"large":{"type":"string"},"small":{"type":"string"}},"required":["small","large"],"type":"object"},"screenshots":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"name":{"type":"string"},"path":{"type":"string"}},"type":"object"},"type":"array"},"updated":{"type":"string"},"version":{"type":"string"}},"required":["keywords","logos","updated","version"],"type":"object"},"JSONData":{"additionalProperties":false,"description":"JSON configuration schema for Grafana plugins\nConverted from: https://github.com/grafana/grafana/blob/main/docs/sources/developers/plugins/plugin.schema.json","properties":{"alerting":{"description":"Optional fields","type":"boolean"},"annotations":{"type":"boolean"},"autoEnabled":{"type":"boolean"},"backend":{"type":"boolean"},"buildMode":{"type":"string"},"builtIn":{"type":"boolean"},"category":{"enum":["tsdb","logging","cloud","tracing","profiling","sql","enterprise","iot","other"],"type":"string"},"dependencies":{"$ref":"#/components/schemas/Dependencies","description":"Dependency information"},"enterpriseFeatures":{"$ref":"#/components/schemas/EnterpriseFeatures"},"executable":{"type":"string"},"extensions":{"$ref":"#/components/schemas/Extensions"},"hideFromList":{"type":"boolean"},"iam":{"$ref":"#/components/schemas/IAM"},"id":{"description":"Unique name of the plugin","type":"string"},"includes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Include"},"type":"array"},"info":{"$ref":"#/components/schemas/Info","description":"Metadata for the plugin"},"logs":{"type":"boolean"},"metrics":{"type":"boolean"},"multiValueFilterOperators":{"type":"boolean"},"name":{"description":"Human-readable name of the plugin","type":"string"},"pascalName":{"type":"string"},"preload":{"type":"boolean"},"queryOptions":{"$ref":"#/components/schemas/QueryOptions"},"roles":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Role"},"type":"array"},"routes":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Route"},"type":"array"},"skipDataQuery":{"type":"boolean"},"state":{"enum":["alpha","beta"],"type":"string"},"streaming":{"type":"boolean"},"suggestions":{"type":"boolean"},"tracing":{"type":"boolean"},"type":{"description":"Plugin type","enum":["app","datasource","panel","renderer"],"type":"string"}},"required":["id","type","name","info","dependencies"],"type":"object"},"Meta":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"lastEvaluation":{"description":"lastEvaluation is the ResourceVersion last evaluated","type":"string"},"state":{"description":"state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.","enum":["success","in_progress","failed"],"type":"string"}},"required":["lastEvaluation","state"],"type":"object"},"QueryOptions":{"additionalProperties":false,"properties":{"cacheTimeout":{"type":"boolean"},"maxDataPoints":{"type":"boolean"},"minInterval":{"type":"boolean"}},"type":"object"},"Role":{"additionalProperties":false,"properties":{"grants":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"role":{"additionalProperties":false,"properties":{"description":{"type":"string"},"name":{"type":"string"},"permissions":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"action":{"type":"string"},"scope":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"}},"type":"object"},"Route":{"additionalProperties":false,"properties":{"body":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"headers":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"jwtTokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"method":{"type":"string"},"path":{"type":"string"},"reqAction":{"type":"string"},"reqRole":{"type":"string"},"reqSignedIn":{"type":"boolean"},"tokenAuth":{"additionalProperties":false,"properties":{"params":{"additionalProperties":{"additionalProperties":{},"type":"object"},"type":"object"},"scopes":{"description":"+listType=set","items":{"type":"string"},"type":"array"},"url":{"type":"string"}},"type":"object"},"url":{"type":"string"},"urlParams":{"description":"+listType=atomic","items":{"additionalProperties":false,"properties":{"content":{"type":"string"},"name":{"type":"string"}},"type":"object"},"type":"array"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"angular":{"additionalProperties":false,"properties":{"detected":{"type":"boolean"}},"required":["detected"],"type":"object"},"baseURL":{"type":"string"},"children":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"class":{"enum":["core","external"],"type":"string"},"module":{"additionalProperties":false,"properties":{"hash":{"type":"string"},"loadingStrategy":{"enum":["fetch","script"],"type":"string"},"path":{"type":"string"}},"required":["path"],"type":"object"},"pluginJson":{"$ref":"#/components/schemas/JSONData"},"signature":{"additionalProperties":false,"properties":{"org":{"type":"string"},"status":{"enum":["internal","valid","invalid","modified","unsigned"],"type":"string"},"type":{"enum":["grafana","commercial","community","private","private-glob"],"type":"string"}},"required":["status"],"type":"object"},"translations":{"additionalProperties":{"type":"string"},"type":"object"}},"required":["pluginJson","class"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"operatorStates":{"additionalProperties":{"$ref":"#/components/schemas/OperatorState"},"description":"operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.","type":"object"}},"type":"object"}}`) versionSchemaMetav0alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaMetav0alpha1, &versionSchemaMetav0alpha1) ) diff --git a/apps/plugins/pkg/app/install/registrar.go b/apps/plugins/pkg/app/install/registrar.go index 3977fd88cf0..a900db1a68d 100644 --- a/apps/plugins/pkg/app/install/registrar.go +++ b/apps/plugins/pkg/app/install/registrar.go @@ -15,16 +15,6 @@ const ( PluginInstallSourceAnnotation = "plugins.grafana.app/install-source" ) -// Class represents the plugin class type in an unversioned internal format. -// This intentionally duplicates the versioned API type (PluginInstallSpecClass) to decouple -// internal code from API version changes, making it easier to support multiple API versions. -type Class = string - -const ( - ClassCore Class = "core" - ClassExternal Class = "external" -) - type Source = string const ( @@ -36,7 +26,6 @@ type PluginInstall struct { ID string Version string URL string - Class Class Source Source } @@ -57,7 +46,6 @@ func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alph Id: p.ID, Version: p.Version, Url: url, - Class: pluginsv0alpha1.PluginSpecClass(p.Class), }, } } @@ -70,9 +58,6 @@ func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.Plugin) bool { if existing.Spec.Version != update.Spec.Version { return true } - if existing.Spec.Class != update.Spec.Class { - return true // this should never really happen - } if !equalStringPointers(existing.Spec.Url, update.Spec.Url) { return true } diff --git a/apps/plugins/pkg/app/install/registrar_test.go b/apps/plugins/pkg/app/install/registrar_test.go index f1e0adbc508..6961acb98f8 100644 --- a/apps/plugins/pkg/app/install/registrar_test.go +++ b/apps/plugins/pkg/app/install/registrar_test.go @@ -26,14 +26,12 @@ func TestPluginInstall_ShouldUpdate(t *testing.T) { Spec: pluginsv0alpha1.PluginSpec{ Id: "plugin-1", Version: "1.0.0", - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, } baseInstall := PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, } @@ -54,13 +52,6 @@ func TestPluginInstall_ShouldUpdate(t *testing.T) { }, expectUpdate: true, }, - { - name: "class differs", - modifyInstall: func(pi *PluginInstall) { - pi.Class = ClassCore - }, - expectUpdate: true, - }, { name: "url differs", modifyInstall: func(pi *PluginInstall) { @@ -109,7 +100,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existingErr: errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1"), @@ -120,7 +110,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "2.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existing: &pluginsv0alpha1.Plugin{ @@ -135,7 +124,6 @@ func TestInstallRegistrar_Register(t *testing.T) { Spec: pluginsv0alpha1.PluginSpec{ Id: "plugin-1", Version: "1.0.0", - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, }, expectedUpdates: 1, @@ -145,7 +133,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existing: &pluginsv0alpha1.Plugin{ @@ -160,7 +147,6 @@ func TestInstallRegistrar_Register(t *testing.T) { Spec: pluginsv0alpha1.PluginSpec{ Id: "plugin-1", Version: "1.0.0", - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, }, }, @@ -169,7 +155,6 @@ func TestInstallRegistrar_Register(t *testing.T) { install: &PluginInstall{ ID: "plugin-err", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, existingErr: errorsK8s.NewInternalError(errors.New("boom")), @@ -410,7 +395,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { install: PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, namespace: "org-1", @@ -424,7 +408,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: "https://example.com/plugin.zip", - Class: ClassExternal, Source: SourcePluginStore, }, namespace: "org-1", @@ -433,25 +416,11 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { require.Equal(t, "https://example.com/plugin.zip", *p.Spec.Url) }, }, - { - name: "core class is mapped correctly", - install: PluginInstall{ - ID: "plugin-core", - Version: "2.0.0", - Class: ClassCore, - Source: SourcePluginStore, - }, - namespace: "org-2", - validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) { - require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCore), p.Spec.Class) - }, - }, { name: "source annotation is set correctly", install: PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourceUnknown, }, namespace: "org-1", @@ -464,7 +433,6 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) { install: PluginInstall{ ID: "my-plugin", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, namespace: "my-namespace", @@ -556,7 +524,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: newURL, - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: nil, @@ -568,7 +535,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: "", - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: &existingURL, @@ -580,7 +546,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: "", - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: nil, @@ -592,7 +557,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { ID: "plugin-1", Version: "1.0.0", URL: existingURL, - Class: ClassExternal, Source: SourcePluginStore, }, existingURL: &existingURL, @@ -614,7 +578,6 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) { Id: "plugin-1", Version: "1.0.0", Url: tt.existingURL, - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, } @@ -670,7 +633,6 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, setupClient: func(fc *fakePluginInstallClient) { @@ -688,7 +650,6 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) { install: &PluginInstall{ ID: "plugin-1", Version: "2.0.0", - Class: ClassExternal, Source: SourcePluginStore, }, setupClient: func(fc *fakePluginInstallClient) { @@ -705,7 +666,6 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) { Spec: pluginsv0alpha1.PluginSpec{ Id: "plugin-1", Version: "1.0.0", - Class: pluginsv0alpha1.PluginSpecClass(ClassExternal), }, }, nil } @@ -876,7 +836,6 @@ func TestInstallRegistrar_GetClientError(t *testing.T) { install := &PluginInstall{ ID: "plugin-1", Version: "1.0.0", - Class: ClassExternal, Source: SourcePluginStore, } diff --git a/apps/plugins/pkg/app/meta/catalog.go b/apps/plugins/pkg/app/meta/catalog.go index 6e6a47fa0bd..481db9b5947 100644 --- a/apps/plugins/pkg/app/meta/catalog.go +++ b/apps/plugins/pkg/app/meta/catalog.go @@ -10,8 +10,6 @@ import ( "time" "github.com/grafana/grafana-app-sdk/logging" - - pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" ) const ( @@ -87,45 +85,9 @@ func (p *CatalogProvider) GetMeta(ctx context.Context, pluginID, version string) return nil, fmt.Errorf("failed to decode response: %w", err) } + metaSpec := grafanaComPluginVersionMetaToMetaSpec(gcomMeta) return &Result{ - Meta: gcomMeta.JSON, + Meta: metaSpec, TTL: p.ttl, }, nil } - -// 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.MetaJSONData `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"` - PackageName string `json:"packageName"` - DownloadURL string `json:"downloadUrl"` - } `json:"any"` - } `json:"packages"` - Links []struct { - Rel string `json:"rel"` - Href string `json:"href"` - } `json:"links"` - AngularDetected bool `json:"angularDetected"` - Scopes []string `json:"scopes"` -} diff --git a/apps/plugins/pkg/app/meta/catalog_test.go b/apps/plugins/pkg/app/meta/catalog_test.go index 845afc7cd54..1dea816e002 100644 --- a/apps/plugins/pkg/app/meta/catalog_test.go +++ b/apps/plugins/pkg/app/meta/catalog_test.go @@ -49,7 +49,7 @@ func TestCatalogProvider_GetMeta(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, expectedMeta, result.Meta) + assert.Equal(t, expectedMeta, result.Meta.PluginJson) assert.Equal(t, defaultCatalogTTL, result.TTL) }) diff --git a/apps/plugins/pkg/app/meta/converter.go b/apps/plugins/pkg/app/meta/converter.go new file mode 100644 index 00000000000..28180a263bd --- /dev/null +++ b/apps/plugins/pkg/app/meta/converter.go @@ -0,0 +1,744 @@ +package meta + +import ( + "encoding/json" + "time" + + 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" +) + +// jsonDataToMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.MetaJSONData. +// nolint:gocyclo +func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSONData { + meta := pluginsv0alpha1.MetaJSONData{ + Id: jsonData.ID, + Name: jsonData.Name, + } + + // Map plugin type + switch jsonData.Type { + case plugins.TypeApp: + meta.Type = pluginsv0alpha1.MetaJSONDataTypeApp + case plugins.TypeDataSource: + meta.Type = pluginsv0alpha1.MetaJSONDataTypeDatasource + case plugins.TypePanel: + meta.Type = pluginsv0alpha1.MetaJSONDataTypePanel + case plugins.TypeRenderer: + meta.Type = pluginsv0alpha1.MetaJSONDataTypeRenderer + } + + // Map Info + meta.Info = pluginsv0alpha1.MetaInfo{ + Keywords: jsonData.Info.Keywords, + Logos: pluginsv0alpha1.MetaV0alpha1InfoLogos{ + 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.MetaV0alpha1InfoAuthor{} + 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.MetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links)) + for _, link := range jsonData.Info.Links { + v0Link := pluginsv0alpha1.MetaV0alpha1InfoLinks{} + 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.MetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots)) + for _, screenshot := range jsonData.Info.Screenshots { + v0Screenshot := pluginsv0alpha1.MetaV0alpha1InfoScreenshots{} + 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.MetaDependencies{ + 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.MetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins)) + for _, dep := range jsonData.Dependencies.Plugins { + var depType pluginsv0alpha1.MetaV0alpha1DependenciesPluginsType + switch dep.Type { + case "app": + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeApp + case "datasource": + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeDatasource + case "panel": + depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypePanel + } + meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.MetaV0alpha1DependenciesPlugins{ + Id: dep.ID, + Type: depType, + Name: dep.Name, + }) + } + } + + if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 { + meta.Dependencies.Extensions = &pluginsv0alpha1.MetaV0alpha1DependenciesExtensions{ + 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.MetaJSONDataCategory + switch jsonData.Category { + case "tsdb": + category = pluginsv0alpha1.MetaJSONDataCategoryTsdb + case "logging": + category = pluginsv0alpha1.MetaJSONDataCategoryLogging + case "cloud": + category = pluginsv0alpha1.MetaJSONDataCategoryCloud + case "tracing": + category = pluginsv0alpha1.MetaJSONDataCategoryTracing + case "profiling": + category = pluginsv0alpha1.MetaJSONDataCategoryProfiling + case "sql": + category = pluginsv0alpha1.MetaJSONDataCategorySql + case "enterprise": + category = pluginsv0alpha1.MetaJSONDataCategoryEnterprise + case "iot": + category = pluginsv0alpha1.MetaJSONDataCategoryIot + case "other": + category = pluginsv0alpha1.MetaJSONDataCategoryOther + default: + category = pluginsv0alpha1.MetaJSONDataCategoryOther + } + meta.Category = &category + } + + // Map state + if jsonData.State != "" { + var state pluginsv0alpha1.MetaJSONDataState + switch jsonData.State { + case plugins.ReleaseStateAlpha: + state = pluginsv0alpha1.MetaJSONDataStateAlpha + case plugins.ReleaseStateBeta: + state = pluginsv0alpha1.MetaJSONDataStateBeta + default: + } + if state != "" { + meta.State = &state + } + } + + // Map executable + if jsonData.Executable != "" { + meta.Executable = &jsonData.Executable + } + + // Map QueryOptions + if len(jsonData.QueryOptions) > 0 { + queryOptions := &pluginsv0alpha1.MetaQueryOptions{} + 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.MetaInclude, 0, len(jsonData.Includes)) + for _, include := range jsonData.Includes { + v0Include := pluginsv0alpha1.MetaInclude{} + if include.UID != "" { + v0Include.Uid = &include.UID + } + if include.Type != "" { + var includeType pluginsv0alpha1.MetaIncludeType + switch include.Type { + case "dashboard": + includeType = pluginsv0alpha1.MetaIncludeTypeDashboard + case "page": + includeType = pluginsv0alpha1.MetaIncludeTypePage + case "panel": + includeType = pluginsv0alpha1.MetaIncludeTypePanel + case "datasource": + includeType = pluginsv0alpha1.MetaIncludeTypeDatasource + } + v0Include.Type = &includeType + } + if include.Name != "" { + v0Include.Name = &include.Name + } + if include.Component != "" { + v0Include.Component = &include.Component + } + if include.Role != "" { + var role pluginsv0alpha1.MetaIncludeRole + switch include.Role { + case "Admin": + role = pluginsv0alpha1.MetaIncludeRoleAdmin + case "Editor": + role = pluginsv0alpha1.MetaIncludeRoleEditor + case "Viewer": + role = pluginsv0alpha1.MetaIncludeRoleViewer + } + 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.MetaRoute, 0, len(jsonData.Routes)) + for _, route := range jsonData.Routes { + v0Route := pluginsv0alpha1.MetaRoute{} + 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.MetaV0alpha1RouteUrlParams, 0, len(route.URLParams)) + for _, param := range route.URLParams { + v0Param := pluginsv0alpha1.MetaV0alpha1RouteUrlParams{} + 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.MetaV0alpha1RouteTokenAuth{} + 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.MetaV0alpha1RouteJwtTokenAuth{} + 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.MetaExtensions{} + + if len(jsonData.Extensions.AddedLinks) > 0 { + extensions.AddedLinks = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks)) + for _, link := range jsonData.Extensions.AddedLinks { + v0Link := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks{ + 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.MetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents)) + for _, comp := range jsonData.Extensions.AddedComponents { + v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents{ + 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.MetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents)) + for _, comp := range jsonData.Extensions.ExposedComponents { + v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents{ + 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.MetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints)) + for _, point := range jsonData.Extensions.ExtensionPoints { + v0Point := pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints{ + 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.MetaRole, 0, len(jsonData.Roles)) + for _, role := range jsonData.Roles { + v0Role := pluginsv0alpha1.MetaRole{ + Grants: role.Grants, + } + if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 { + v0RoleRole := &pluginsv0alpha1.MetaV0alpha1RoleRole{} + 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.MetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions)) + for _, perm := range role.Role.Permissions { + v0Perm := pluginsv0alpha1.MetaV0alpha1RoleRolePermissions{} + 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.MetaIAM{ + Permissions: make([]pluginsv0alpha1.MetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)), + } + for _, perm := range jsonData.IAM.Permissions { + v0Perm := pluginsv0alpha1.MetaV0alpha1IAMPermissions{} + 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 +} + +// pluginStorePluginToMeta converts a pluginstore.Plugin to a pluginsv0alpha1.MetaSpec. +// This is similar to pluginToPluginMetaSpec but works with the plugin store DTO. +// loadingStrategy and moduleHash are optional calculated values that can be provided. +func pluginStorePluginToMeta(plugin pluginstore.Plugin, loadingStrategy plugins.LoadingStrategy, moduleHash string) pluginsv0alpha1.MetaSpec { + metaSpec := pluginsv0alpha1.MetaSpec{ + PluginJson: jsonDataToMetaJSONData(plugin.JSONData), + } + + // Set Class - default to External if not specified + var c pluginsv0alpha1.MetaSpecClass + if plugin.Class == plugins.ClassCore { + c = pluginsv0alpha1.MetaSpecClassCore + } else { + c = pluginsv0alpha1.MetaSpecClassExternal + } + metaSpec.Class = c + + if plugin.Module != "" { + module := &pluginsv0alpha1.MetaV0alpha1SpecModule{ + Path: plugin.Module, + } + if moduleHash != "" { + module.Hash = &moduleHash + } + if loadingStrategy != "" { + var ls pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategy + switch loadingStrategy { + case plugins.LoadingStrategyFetch: + ls = pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyFetch + case plugins.LoadingStrategyScript: + ls = pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyScript + } + module.LoadingStrategy = &ls + } + metaSpec.Module = module + } + + if plugin.BaseURL != "" { + metaSpec.BaseURL = &plugin.BaseURL + } + + if plugin.Signature != "" { + signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{ + Status: convertSignatureStatus(plugin.Signature), + } + + if plugin.SignatureType != "" { + sigType := convertSignatureType(plugin.SignatureType) + signature.Type = &sigType + } + + if plugin.SignatureOrg != "" { + signature.Org = &plugin.SignatureOrg + } + + metaSpec.Signature = signature + } + + if len(plugin.Children) > 0 { + metaSpec.Children = plugin.Children + } + + metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ + Detected: plugin.Angular.Detected, + } + + if len(plugin.Translations) > 0 { + metaSpec.Translations = plugin.Translations + } + + return metaSpec +} + +// convertSignatureStatus converts plugins.SignatureStatus to pluginsv0alpha1.MetaV0alpha1SpecSignatureStatus. +func convertSignatureStatus(status plugins.SignatureStatus) pluginsv0alpha1.MetaV0alpha1SpecSignatureStatus { + switch status { + case plugins.SignatureStatusInternal: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusInternal + case plugins.SignatureStatusValid: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusValid + case plugins.SignatureStatusInvalid: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusInvalid + case plugins.SignatureStatusModified: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusModified + case plugins.SignatureStatusUnsigned: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusUnsigned + default: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusUnsigned + } +} + +// convertSignatureType converts plugins.SignatureType to pluginsv0alpha1.MetaV0alpha1SpecSignatureType. +func convertSignatureType(sigType plugins.SignatureType) pluginsv0alpha1.MetaV0alpha1SpecSignatureType { + switch sigType { + case plugins.SignatureTypeGrafana: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana + case plugins.SignatureTypeCommercial: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommercial + case plugins.SignatureTypeCommunity: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommunity + case plugins.SignatureTypePrivate: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivate + case plugins.SignatureTypePrivateGlob: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivateGlob + default: + return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana + } +} + +// pluginToMetaSpec converts a fully loaded *plugins.Plugin to a pluginsv0alpha1.MetaSpec. +func pluginToMetaSpec(plugin *plugins.Plugin) pluginsv0alpha1.MetaSpec { + metaSpec := pluginsv0alpha1.MetaSpec{ + PluginJson: jsonDataToMetaJSONData(plugin.JSONData), + } + + // Set Class - default to External if not specified + var c pluginsv0alpha1.MetaSpecClass + if plugin.Class == plugins.ClassCore { + c = pluginsv0alpha1.MetaSpecClassCore + } else { + c = pluginsv0alpha1.MetaSpecClassExternal + } + metaSpec.Class = c + + // Set module information + if plugin.Module != "" { + module := &pluginsv0alpha1.MetaV0alpha1SpecModule{ + Path: plugin.Module, + } + + loadingStrategy := pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyScript + module.LoadingStrategy = &loadingStrategy + + metaSpec.Module = module + } + + // Set BaseURL + if plugin.BaseURL != "" { + metaSpec.BaseURL = &plugin.BaseURL + } + + // Set signature information + signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{ + Status: convertSignatureStatus(plugin.Signature), + } + + if plugin.SignatureType != "" { + sigType := convertSignatureType(plugin.SignatureType) + signature.Type = &sigType + } + + if plugin.SignatureOrg != "" { + signature.Org = &plugin.SignatureOrg + } + + metaSpec.Signature = signature + + if len(plugin.Children) > 0 { + children := make([]string, 0, len(plugin.Children)) + for _, child := range plugin.Children { + children = append(children, child.ID) + } + metaSpec.Children = children + } + + metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ + Detected: plugin.Angular.Detected, + } + + if len(plugin.Translations) > 0 { + metaSpec.Translations = plugin.Translations + } + + return metaSpec +} + +// 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.MetaJSONData `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"` + PackageName string `json:"packageName"` + DownloadURL string `json:"downloadUrl"` + } `json:"any"` + } `json:"packages"` + Links []struct { + Rel string `json:"rel"` + Href string `json:"href"` + } `json:"links"` + AngularDetected bool `json:"angularDetected"` + Scopes []string `json:"scopes"` +} + +// grafanaComPluginVersionMetaToMetaSpec converts a grafanaComPluginVersionMeta to a pluginsv0alpha1.MetaSpec. +func grafanaComPluginVersionMetaToMetaSpec(gcomMeta grafanaComPluginVersionMeta) pluginsv0alpha1.MetaSpec { + metaSpec := pluginsv0alpha1.MetaSpec{ + PluginJson: gcomMeta.JSON, + Class: pluginsv0alpha1.MetaSpecClassExternal, + } + + if gcomMeta.SignatureType != "" { + signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{ + Status: pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusValid, + } + + switch gcomMeta.SignatureType { + case "grafana": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana + signature.Type = &sigType + case "commercial": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommercial + signature.Type = &sigType + case "community": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommunity + signature.Type = &sigType + case "private": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivate + signature.Type = &sigType + case "private-glob": + sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivateGlob + signature.Type = &sigType + } + + if gcomMeta.SignedByOrg != "" { + signature.Org = &gcomMeta.SignedByOrg + } + + metaSpec.Signature = signature + } + + // Set angular info + metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{ + Detected: gcomMeta.AngularDetected, + } + + return metaSpec +} diff --git a/apps/plugins/pkg/app/meta/core.go b/apps/plugins/pkg/app/meta/core.go index e16f7a23db5..9fb167d5578 100644 --- a/apps/plugins/pkg/app/meta/core.go +++ b/apps/plugins/pkg/app/meta/core.go @@ -2,7 +2,6 @@ package meta import ( "context" - "encoding/json" "errors" "os" "path/filepath" @@ -13,7 +12,15 @@ import ( pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + pluginsLoader "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation" "github.com/grafana/grafana/pkg/plugins/manager/sources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" ) const ( @@ -23,9 +30,10 @@ const ( // CoreProvider retrieves plugin metadata for core plugins. type CoreProvider struct { mu sync.RWMutex - loadedPlugins map[string]pluginsv0alpha1.MetaJSONData + loadedPlugins map[string]pluginsv0alpha1.MetaSpec initialized bool ttl time.Duration + loader pluginsLoader.Service } // NewCoreProvider creates a new CoreProvider for core plugins. @@ -35,9 +43,13 @@ func NewCoreProvider() *CoreProvider { // NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL. func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider { + cfg := &config.PluginManagementCfg{ + Features: config.Features{}, + } return &CoreProvider{ - loadedPlugins: make(map[string]pluginsv0alpha1.MetaJSONData), + loadedPlugins: make(map[string]pluginsv0alpha1.MetaSpec), ttl: ttl, + loader: createLoader(cfg), } } @@ -76,9 +88,9 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result p.initialized = true } - if meta, found := p.loadedPlugins[pluginID]; found { + if spec, found := p.loadedPlugins[pluginID]; found { return &Result{ - Meta: meta, + Meta: spec, TTL: p.ttl, }, nil } @@ -86,8 +98,8 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result return nil, ErrMetaNotFound } -// loadPlugins discovers and caches all core plugins. -// Returns an error if the static root path cannot be found or if plugin discovery fails. +// loadPlugins discovers and caches all core plugins by fully loading them. +// Returns an error if the static root path cannot be found or if plugin loading fails. // This error will be handled gracefully by GetMeta, which will return ErrMetaNotFound // to allow other providers to handle the request. func (p *CoreProvider) loadPlugins(ctx context.Context) error { @@ -108,496 +120,51 @@ func (p *CoreProvider) loadPlugins(ctx context.Context) error { panelPath := filepath.Join(staticRootPath, "app", "plugins", "panel") src := sources.NewLocalSource(plugins.ClassCore, []string{datasourcePath, panelPath}) - ps, err := src.Discover(ctx) + loadedPlugins, err := p.loader.Load(ctx, src) if err != nil { return err } - if len(ps) == 0 { - logging.DefaultLogger.Warn("CoreProvider: no core plugins found during discovery") + if len(loadedPlugins) == 0 { + logging.DefaultLogger.Warn("CoreProvider: no core plugins found during loading") return nil } - for _, bundle := range ps { - meta := jsonDataToMetaJSONData(bundle.Primary.JSONData) - p.loadedPlugins[bundle.Primary.JSONData.ID] = meta + for _, plugin := range loadedPlugins { + metaSpec := pluginToMetaSpec(plugin) + p.loadedPlugins[plugin.ID] = metaSpec } return nil } -// jsonDataToMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.MetaJSONData. -// nolint:gocyclo -func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSONData { - meta := pluginsv0alpha1.MetaJSONData{ - Id: jsonData.ID, - Name: jsonData.Name, - } - - // Map plugin type - switch jsonData.Type { - case plugins.TypeApp: - meta.Type = pluginsv0alpha1.MetaJSONDataTypeApp - case plugins.TypeDataSource: - meta.Type = pluginsv0alpha1.MetaJSONDataTypeDatasource - case plugins.TypePanel: - meta.Type = pluginsv0alpha1.MetaJSONDataTypePanel - case plugins.TypeRenderer: - meta.Type = pluginsv0alpha1.MetaJSONDataTypeRenderer - } - - // Map Info - meta.Info = pluginsv0alpha1.MetaInfo{ - Keywords: jsonData.Info.Keywords, - Logos: pluginsv0alpha1.MetaV0alpha1InfoLogos{ - Small: jsonData.Info.Logos.Small, - Large: jsonData.Info.Logos.Large, +// createLoader creates a loader service configured for core plugins. +func createLoader(cfg *config.PluginManagementCfg) pluginsLoader.Service { + d := discovery.New(cfg, discovery.Opts{ + FilterFuncs: []discovery.FilterFunc{ + // Allow all plugin types for core plugins }, - Updated: jsonData.Info.Updated, - Version: jsonData.Info.Version, - } + }) + b := bootstrap.New(cfg, bootstrap.Opts{ + DecorateFuncs: []bootstrap.DecorateFunc{}, // no decoration required for metadata + }) + v := validation.New(cfg, validation.Opts{ + ValidateFuncs: []validation.ValidateFunc{ + // Skip validation for core plugins - they're trusted + }, + }) + i := initialization.New(cfg, initialization.Opts{ + InitializeFuncs: []initialization.InitializeFunc{ + // Skip initialization - we only need metadata, not running plugins + }, + }) + t, _ := termination.New(cfg, termination.Opts{ + TerminateFuncs: []termination.TerminateFunc{ + // No termination needed for metadata-only loading + }, + }) - if jsonData.Info.Description != "" { - meta.Info.Description = &jsonData.Info.Description - } + et := pluginerrs.ProvideErrorTracker() - if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" { - author := &pluginsv0alpha1.MetaV0alpha1InfoAuthor{} - 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.MetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links)) - for _, link := range jsonData.Info.Links { - v0Link := pluginsv0alpha1.MetaV0alpha1InfoLinks{} - 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.MetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots)) - for _, screenshot := range jsonData.Info.Screenshots { - v0Screenshot := pluginsv0alpha1.MetaV0alpha1InfoScreenshots{} - 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.MetaDependencies{ - 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.MetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins)) - for _, dep := range jsonData.Dependencies.Plugins { - var depType pluginsv0alpha1.MetaV0alpha1DependenciesPluginsType - switch dep.Type { - case "app": - depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeApp - case "datasource": - depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeDatasource - case "panel": - depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypePanel - } - meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.MetaV0alpha1DependenciesPlugins{ - Id: dep.ID, - Type: depType, - Name: dep.Name, - }) - } - } - - if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 { - meta.Dependencies.Extensions = &pluginsv0alpha1.MetaV0alpha1DependenciesExtensions{ - 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.MetaJSONDataCategory - switch jsonData.Category { - case "tsdb": - category = pluginsv0alpha1.MetaJSONDataCategoryTsdb - case "logging": - category = pluginsv0alpha1.MetaJSONDataCategoryLogging - case "cloud": - category = pluginsv0alpha1.MetaJSONDataCategoryCloud - case "tracing": - category = pluginsv0alpha1.MetaJSONDataCategoryTracing - case "profiling": - category = pluginsv0alpha1.MetaJSONDataCategoryProfiling - case "sql": - category = pluginsv0alpha1.MetaJSONDataCategorySql - case "enterprise": - category = pluginsv0alpha1.MetaJSONDataCategoryEnterprise - case "iot": - category = pluginsv0alpha1.MetaJSONDataCategoryIot - case "other": - category = pluginsv0alpha1.MetaJSONDataCategoryOther - default: - category = pluginsv0alpha1.MetaJSONDataCategoryOther - } - meta.Category = &category - } - - // Map state - if jsonData.State != "" { - var state pluginsv0alpha1.MetaJSONDataState - switch jsonData.State { - case plugins.ReleaseStateAlpha: - state = pluginsv0alpha1.MetaJSONDataStateAlpha - case plugins.ReleaseStateBeta: - state = pluginsv0alpha1.MetaJSONDataStateBeta - default: - } - if state != "" { - meta.State = &state - } - } - - // Map executable - if jsonData.Executable != "" { - meta.Executable = &jsonData.Executable - } - - // Map QueryOptions - if len(jsonData.QueryOptions) > 0 { - queryOptions := &pluginsv0alpha1.MetaQueryOptions{} - 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.MetaInclude, 0, len(jsonData.Includes)) - for _, include := range jsonData.Includes { - v0Include := pluginsv0alpha1.MetaInclude{} - if include.UID != "" { - v0Include.Uid = &include.UID - } - if include.Type != "" { - var includeType pluginsv0alpha1.MetaIncludeType - switch include.Type { - case "dashboard": - includeType = pluginsv0alpha1.MetaIncludeTypeDashboard - case "page": - includeType = pluginsv0alpha1.MetaIncludeTypePage - case "panel": - includeType = pluginsv0alpha1.MetaIncludeTypePanel - case "datasource": - includeType = pluginsv0alpha1.MetaIncludeTypeDatasource - } - v0Include.Type = &includeType - } - if include.Name != "" { - v0Include.Name = &include.Name - } - if include.Component != "" { - v0Include.Component = &include.Component - } - if include.Role != "" { - var role pluginsv0alpha1.MetaIncludeRole - switch include.Role { - case "Admin": - role = pluginsv0alpha1.MetaIncludeRoleAdmin - case "Editor": - role = pluginsv0alpha1.MetaIncludeRoleEditor - case "Viewer": - role = pluginsv0alpha1.MetaIncludeRoleViewer - } - 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.MetaRoute, 0, len(jsonData.Routes)) - for _, route := range jsonData.Routes { - v0Route := pluginsv0alpha1.MetaRoute{} - 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.MetaV0alpha1RouteUrlParams, 0, len(route.URLParams)) - for _, param := range route.URLParams { - v0Param := pluginsv0alpha1.MetaV0alpha1RouteUrlParams{} - 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.MetaV0alpha1RouteTokenAuth{} - 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.MetaV0alpha1RouteJwtTokenAuth{} - 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.MetaExtensions{} - - if len(jsonData.Extensions.AddedLinks) > 0 { - extensions.AddedLinks = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks)) - for _, link := range jsonData.Extensions.AddedLinks { - v0Link := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks{ - 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.MetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents)) - for _, comp := range jsonData.Extensions.AddedComponents { - v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents{ - 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.MetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents)) - for _, comp := range jsonData.Extensions.ExposedComponents { - v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents{ - 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.MetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints)) - for _, point := range jsonData.Extensions.ExtensionPoints { - v0Point := pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints{ - 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.MetaRole, 0, len(jsonData.Roles)) - for _, role := range jsonData.Roles { - v0Role := pluginsv0alpha1.MetaRole{ - Grants: role.Grants, - } - if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 { - v0RoleRole := &pluginsv0alpha1.MetaV0alpha1RoleRole{} - 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.MetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions)) - for _, perm := range role.Role.Permissions { - v0Perm := pluginsv0alpha1.MetaV0alpha1RoleRolePermissions{} - 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.MetaIAM{ - Permissions: make([]pluginsv0alpha1.MetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)), - } - for _, perm := range jsonData.IAM.Permissions { - v0Perm := pluginsv0alpha1.MetaV0alpha1IAMPermissions{} - 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 + return pluginsLoader.New(cfg, d, b, v, i, t, et) } diff --git a/apps/plugins/pkg/app/meta/core_test.go b/apps/plugins/pkg/app/meta/core_test.go index d5235c9d120..d6f51057fab 100644 --- a/apps/plugins/pkg/app/meta/core_test.go +++ b/apps/plugins/pkg/app/meta/core_test.go @@ -22,10 +22,12 @@ func TestCoreProvider_GetMeta(t *testing.T) { t.Run("returns cached plugin when available", func(t *testing.T) { provider := NewCoreProvider() - expectedMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider.mu.Lock() @@ -58,10 +60,12 @@ func TestCoreProvider_GetMeta(t *testing.T) { t.Run("ignores version parameter", func(t *testing.T) { provider := NewCoreProvider() - expectedMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider.mu.Lock() @@ -81,10 +85,12 @@ func TestCoreProvider_GetMeta(t *testing.T) { customTTL := 2 * time.Hour provider := NewCoreProviderWithTTL(customTTL) - expectedMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider.mu.Lock() @@ -226,8 +232,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.Id) - assert.Equal(t, "Test Datasource", result.Meta.Name) + assert.Equal(t, "test-datasource", result.Meta.PluginJson.Id) + assert.Equal(t, "Test Datasource", result.Meta.PluginJson.Name) } }) } diff --git a/apps/plugins/pkg/app/meta/local.go b/apps/plugins/pkg/app/meta/local.go new file mode 100644 index 00000000000..2c699520cfc --- /dev/null +++ b/apps/plugins/pkg/app/meta/local.go @@ -0,0 +1,53 @@ +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 := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash) + return &Result{ + Meta: spec, + TTL: defaultLocalTTL, + }, nil +} diff --git a/apps/plugins/pkg/app/meta/manager.go b/apps/plugins/pkg/app/meta/manager.go index 7e99d0cc197..d8cd1f554c2 100644 --- a/apps/plugins/pkg/app/meta/manager.go +++ b/apps/plugins/pkg/app/meta/manager.go @@ -16,7 +16,7 @@ const ( // cachedMeta represents a cached metadata entry with expiration time type cachedMeta struct { - meta pluginsv0alpha1.MetaJSONData + meta pluginsv0alpha1.MetaSpec 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 { - continue + return result, nil } pm.cacheMu.Lock() diff --git a/apps/plugins/pkg/app/meta/manager_test.go b/apps/plugins/pkg/app/meta/manager_test.go index 31a75424a23..c9a8a9f3a5f 100644 --- a/apps/plugins/pkg/app/meta/manager_test.go +++ b/apps/plugins/pkg/app/meta/manager_test.go @@ -35,10 +35,12 @@ 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.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + cachedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider := &mockProvider{ @@ -60,8 +62,10 @@ func TestProviderManager_GetMeta(t *testing.T) { provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) { return &Result{ - Meta: pluginsv0alpha1.MetaJSONData{Id: "different"}, - TTL: time.Hour, + Meta: pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{Id: "different"}, + }, + TTL: time.Hour, }, nil } @@ -73,10 +77,12 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("fetches from provider when not cached", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } expectedTTL := 2 * time.Hour @@ -107,19 +113,16 @@ func TestProviderManager_GetMeta(t *testing.T) { assert.Equal(t, expectedTTL, cached.ttl) }) - t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) { - zeroTTLMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Zero TTL Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, - } - expectedMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + t.Run("does not cache result with zero TTL", func(t *testing.T) { + zeroTTLMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Zero TTL Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } - provider1 := &mockProvider{ + provider := &mockProvider{ getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { return &Result{ Meta: zeroTTLMeta, @@ -127,37 +130,30 @@ 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(provider1, provider2) + pm := NewProviderManager(provider) result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0") require.NoError(t, err) require.NotNil(t, result) - assert.Equal(t, expectedMeta, result.Meta) + assert.Equal(t, zeroTTLMeta, result.Meta) + assert.Equal(t, time.Duration(0), result.TTL) pm.cacheMu.RLock() - cached, exists := pm.cache["test-plugin:1.0.0"] + _, exists := pm.cache["test-plugin:1.0.0"] pm.cacheMu.RUnlock() - assert.True(t, exists) - assert.Equal(t, expectedMeta, cached.meta) - assert.Equal(t, time.Hour, cached.ttl) + assert.False(t, exists, "zero TTL results should not be cached") }) t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) { - expectedMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider1 := &mockProvider{ @@ -229,15 +225,19 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("skips expired cache entries", func(t *testing.T) { - expiredMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Expired Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expiredMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Expired Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } - expectedMeta := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Test Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Test Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } callCount := 0 @@ -272,15 +272,19 @@ func TestProviderManager_GetMeta(t *testing.T) { }) t.Run("uses first successful provider", func(t *testing.T) { - expectedMeta1 := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Provider 1 Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta1 := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Provider 1 Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } - expectedMeta2 := pluginsv0alpha1.MetaJSONData{ - Id: "test-plugin", - Name: "Provider 2 Plugin", - Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + expectedMeta2 := pluginsv0alpha1.MetaSpec{ + PluginJson: pluginsv0alpha1.MetaJSONData{ + Id: "test-plugin", + Name: "Provider 2 Plugin", + Type: pluginsv0alpha1.MetaJSONDataTypeDatasource, + }, } provider1 := &mockProvider{ @@ -331,9 +335,9 @@ func TestProviderManager_Run(t *testing.T) { func TestProviderManager_cleanupExpired(t *testing.T) { t.Run("removes expired entries", func(t *testing.T) { - validMeta := pluginsv0alpha1.MetaJSONData{Id: "valid"} - expiredMeta1 := pluginsv0alpha1.MetaJSONData{Id: "expired1"} - expiredMeta2 := pluginsv0alpha1.MetaJSONData{Id: "expired2"} + validMeta := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "valid"}} + expiredMeta1 := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "expired1"}} + expiredMeta2 := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "expired2"}} provider := &mockProvider{ getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) { diff --git a/apps/plugins/pkg/app/meta/provider.go b/apps/plugins/pkg/app/meta/provider.go index 818c0da9dc5..3c01361d656 100644 --- a/apps/plugins/pkg/app/meta/provider.go +++ b/apps/plugins/pkg/app/meta/provider.go @@ -14,7 +14,7 @@ var ( // Result contains plugin metadata along with its recommended TTL. type Result struct { - Meta pluginsv0alpha1.MetaJSONData + Meta pluginsv0alpha1.MetaSpec TTL time.Duration } diff --git a/apps/plugins/pkg/app/storage.go b/apps/plugins/pkg/app/storage.go index f2dd2ac98d5..01e62b9c4ec 100644 --- a/apps/plugins/pkg/app/storage.go +++ b/apps/plugins/pkg/app/storage.go @@ -121,8 +121,19 @@ func (s *MetaStorage) List(ctx context.Context, options *internalversion.ListOpt continue } - pluginMeta := createMetaFromMetaJSONData(result.Meta, plugin.Name, plugin.Namespace) - metaItems = append(metaItems, *pluginMeta) + pluginMeta := pluginsv0alpha1.Meta{ + ObjectMeta: metav1.ObjectMeta{ + Name: plugin.Name, + Namespace: plugin.Namespace, + }, + Spec: result.Meta, + } + pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{ + Group: pluginsv0alpha1.APIGroup, + Version: pluginsv0alpha1.APIVersion, + Kind: pluginsv0alpha1.MetaKind().Kind(), + }) + metaItems = append(metaItems, pluginMeta) } list := &pluginsv0alpha1.MetaList{ @@ -169,27 +180,18 @@ func (s *MetaStorage) Get(ctx context.Context, name string, options *metav1.GetO return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err)) } - return createMetaFromMetaJSONData(result.Meta, name, ns.Value), nil -} - -// createMetaFromMetaJSONData creates a Meta k8s object from MetaJSONData and plugin metadata. -func createMetaFromMetaJSONData(pluginJSON pluginsv0alpha1.MetaJSONData, name, namespace string) *pluginsv0alpha1.Meta { pluginMeta := &pluginsv0alpha1.Meta{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: pluginsv0alpha1.MetaSpec{ - PluginJSON: pluginJSON, + Name: plugin.Name, + Namespace: plugin.Namespace, }, + Spec: result.Meta, } - - // Set the GroupVersionKind pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{ Group: pluginsv0alpha1.APIGroup, Version: pluginsv0alpha1.APIVersion, Kind: pluginsv0alpha1.MetaKind().Kind(), }) - return pluginMeta + return pluginMeta, nil } diff --git a/pkg/registry/apps/plugins/register.go b/pkg/registry/apps/plugins/register.go index 9113d927a29..68082f59713 100644 --- a/pkg/registry/apps/plugins/register.go +++ b/pkg/registry/apps/plugins/register.go @@ -2,16 +2,19 @@ package plugins import ( "fmt" - "os" authlib "github.com/grafana/authlib/types" appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app" "github.com/grafana/grafana/apps/plugins/pkg/app/meta" + "github.com/grafana/grafana/pkg/configprovider" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/appinstaller" grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" ) var ( @@ -20,23 +23,26 @@ var ( ) type AppInstaller struct { + metaManager *meta.ProviderManager + cfgProvider configprovider.ConfigProvider + restConfigProvider apiserver.RestConfigProvider + *pluginsapp.PluginAppInstaller } -func ProvideAppInstaller(accessControlService accesscontrol.Service, accessClient authlib.AccessClient) (*AppInstaller, error) { +func ProvideAppInstaller( + cfgProvider configprovider.ConfigProvider, + restConfigProvider apiserver.RestConfigProvider, + pluginStore pluginstore.Store, + pluginAssetsService *pluginassets.Service, + accessControlService accesscontrol.Service, accessClient authlib.AccessClient, +) (*AppInstaller, error) { if err := registerAccessControlRoles(accessControlService); err != nil { return nil, fmt.Errorf("registering access control roles: %w", err) } - grafanaComAPIURL := os.Getenv("GRAFANA_COM_API_URL") - if grafanaComAPIURL == "" { - grafanaComAPIURL = "https://grafana.com/api/plugins" - } - - coreProvider := meta.NewCoreProvider() - cloudProvider := meta.NewCatalogProvider(grafanaComAPIURL) - metaProviderManager := meta.NewProviderManager(coreProvider, cloudProvider) - + localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService) + metaProviderManager := meta.NewProviderManager(localProvider) authorizer := grafanaauthorizer.NewResourceAuthorizer(accessClient) i, err := pluginsapp.ProvideAppInstaller(authorizer, metaProviderManager) if err != nil { @@ -44,6 +50,9 @@ func ProvideAppInstaller(accessControlService accesscontrol.Service, accessClien } return &AppInstaller{ + metaManager: metaProviderManager, + cfgProvider: cfgProvider, + restConfigProvider: restConfigProvider, PluginAppInstaller: i, }, nil } diff --git a/pkg/registry/apps/wireset.go b/pkg/registry/apps/wireset.go index a91fa138309..a2cef7b2d55 100644 --- a/pkg/registry/apps/wireset.go +++ b/pkg/registry/apps/wireset.go @@ -2,7 +2,6 @@ package appregistry import ( "github.com/google/wire" - "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/grafana/grafana/pkg/registry/apps/alerting/historian" "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications" @@ -14,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apps/logsdrilldown" "github.com/grafana/grafana/pkg/registry/apps/playlist" "github.com/grafana/grafana/pkg/registry/apps/plugins" + "github.com/grafana/grafana/pkg/registry/apps/quotas" "github.com/grafana/grafana/pkg/registry/apps/shorturl" ) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 5ed04adf12b..478ef749f11 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -786,7 +786,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) + appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient) if err != nil { return nil, err } @@ -1447,7 +1447,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - appInstaller, err := plugins.ProvideAppInstaller(acimplService, accessClient) + appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient) if err != nil { return nil, err } diff --git a/pkg/services/pluginsintegration/installsync/syncer.go b/pkg/services/pluginsintegration/installsync/syncer.go index 6352f7900fa..7c483d21d01 100644 --- a/pkg/services/pluginsintegration/installsync/syncer.go +++ b/pkg/services/pluginsintegration/installsync/syncer.go @@ -217,7 +217,6 @@ func (s *syncer) syncNamespace(ctx context.Context, namespace string, source ins err := s.installRegistrar.Register(ctx, namespace, &install.PluginInstall{ ID: p.ID, Version: p.Info.Version, - Class: install.Class(p.Class), Source: source, }) if err != nil { diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go index 8dccc2c3522..f4504d254c5 100644 --- a/pkg/services/pluginsintegration/pluginstore/plugins.go +++ b/pkg/services/pluginsintegration/pluginstore/plugins.go @@ -17,6 +17,7 @@ type Plugin struct { // App fields Parent *ParentPlugin + Children []string IncludedInAppID string DefaultNavURL string Pinned bool @@ -85,6 +86,18 @@ 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 } diff --git a/pkg/tests/apis/plugins/metas_test.go b/pkg/tests/apis/plugins/metas_test.go index c240d3fb91d..0132ee72173 100644 --- a/pkg/tests/apis/plugins/metas_test.go +++ b/pkg/tests/apis/plugins/metas_test.go @@ -28,7 +28,7 @@ func TestIntegrationPluginMeta(t *testing.T) { "apiVersion": "plugins.grafana.app/v0alpha1", "kind": "Plugin", "metadata": {"name": "%s"}, - "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} + "spec": {"id": "piechart", "version": "1.0.0"} }`, plugin1Name)) _, err := client.Resource.Create(ctx, plugin1, metav1.CreateOptions{}) require.NoError(t, err) @@ -38,7 +38,7 @@ func TestIntegrationPluginMeta(t *testing.T) { "apiVersion": "plugins.grafana.app/v0alpha1", "kind": "Plugin", "metadata": {"name": "%s"}, - "spec": {"id": "grafana-clock-panel", "version": "1.0.0"} + "spec": {"id": "table", "version": "1.0.0"} }`, plugin2Name)) _, err = client.Resource.Create(ctx, plugin2, metav1.CreateOptions{}) require.NoError(t, err) @@ -57,14 +57,14 @@ func TestIntegrationPluginMeta(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"]) + require.True(t, foundIDs["piechart"]) + require.True(t, foundIDs["table"]) }) t.Run("list plugin metas with no plugins", func(t *testing.T) { @@ -95,7 +95,7 @@ func TestIntegrationPluginMeta(t *testing.T) { "apiVersion": "plugins.grafana.app/v0alpha1", "kind": "Plugin", "metadata": {"name": "%s"}, - "spec": {"id": "grafana-piechart-panel", "version": "1.0.0"} + "spec": {"id": "piechart", "version": "1.0.0"} }`, pluginName)) _, err := client.Resource.Create(ctx, plugin, metav1.CreateOptions{}) require.NoError(t, err) @@ -109,10 +109,10 @@ func TestIntegrationPluginMeta(t *testing.T) { }, &pluginsv0alpha1.Meta{}) 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, "piechart", 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) {