mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 16:12:14 +08:00
Plugins: Allow loading panel plugins from a CDN (#59096)
* POC: Plugins CDN reverse proxy * CDN proxy POC: changed env var names * Add authorization: false for /public path in frontend plugin loader * Moved CDN settings to Cfg, add some comments * Fix error 500 in asset fetch if plugin is not using CDN * Fix EnterpriseLicensePath declared twice * Fix linter complaining about whitespaces * Plugins CDN: Skip signature verification for CDN plugins * Plugins CDN: Skip manifest and signature check for cdn plugins * Plugins: use IsValid() and IsInternal() rather than equality checks * Plugins CDN: remove comment * Plugins CDN: Fix seeker can't seek when serving plugins from local fs * Plugins CDN: add back error codes in getLocalPluginAssets * Plugins CDN: call asset.Close() rather than asset.readSeekCloser.Close() * Plugins CDN: Fix panic in JsonApiErr when errorMessageCoder wraps a nil error * Plugins CDN: Add error handling to proxyCDNPluginAsset * Plugins CDN: replace errorMessageCoder with errutil * Plugins CDN POC: expose cdn plugin paths to frontend for system.js * Plugins CDN: Fix cdn plugins showing as unsigned in frontend * WIP: Add support for formatted URL * Fix missing cdnPluginsBaseURLs in GrafanaConfig * Plugins CDN: Remove reverse proxy mode and reverse proxy references * Plugins CDN: Simplify asset serving logic * Plugins CDN: sanitize redirect path * Plugins CDN: Removed unused pluginAsset type * Plugins CDN: Removed system.js changes * Plugins CDN: Return different system.js baseURL and module for cdn plugins * Plugins CDN: Ensure CDN is disabled for non-external plugins * lint * Plugins CDN: serve images and screenshots from CDN, refactoring * Lint * Plugins CDN: Fix URLs for system.js (baseUrl and module) * Plugins CDN: Add more tests for RelativeURLForSystemJS * Plugins CDN: Iterate only on apps when preloading * Plugins CDN: Refactoring * Plugins CDN: Add comments to url_constructor.go * Plugins CDN: Update defaultHGPluginsCDNBaseURL * Plugins CDN: undo extract meta from system js config * refactor(plugins): migrate systemjs css plugin to typescript * feat(plugins): introduce systemjs cdn loader plugin * feat(plugins): add systemjs load type * Plugins CDN: Removed RelativeURLForSystemJS * Plugins CDN: Log backend redirect hits along with plugin info * Plugins CDN: Add pluginsCDNBasePath to getFrontendSettingsMap * feat(plugins): introduce cdn loading for angular plugins * refactor(plugins): move systemjs cache buster into systemjsplugins directory * Plugins CDN: Rename pluginsCDNBasePath to pluginsCDNBaseURL * refactor(plugins): introduce pluginsCDNBaseURL to the frontend * Plugins CDN: Renamed "cdn base path" to "cdn url template" in backend * Plugins CDN: lint * merge with main * Instrumentation: Add prometheus counter for backend hits, log from Info to Warn * Config: Changed key from plugins_cdn.url to plugins.plugins_cdn_base_url * CDN: Add backend tests * Lint: goimports * Default CDN URL to empty string, * Do not use CDN in setImages and module if the url template is empty * CDN: Backend: Add test for frontend settings * CDN: Do not log missing module.js warn if plugin is being loaded from CDN * CDN: Add backend test for CDN plugin loader * Removed 'cdn' signature level, switch to 'valid' * Fix pfs.TestParseTreeTestdata for cdn plugin testdata dir * Fix TestLoader_Load * Fix gocyclo complexity of loadPlugins * Plugins CDN: Moved prometheus metric to api package, removed asset_path label * Fix missing in config * Changes after review * Add pluginscdn.Service * Fix tests * Refactoring * Moved all remaining CDN checks inside pluginscdn.Service * CDN url constructor: Renamed stringURLFor to stringPath * CDN: Moved asset URL functionality to assetpath service * CDN: Renamed HasCDN() to IsEnabled() * CDN: Replace assert with require * CDN: Changes after review * Assetpath: Handle url.Parse error * Fix plugin_resource_test * CDN: Change fallback redirect from 302 to 307 * goimports * Fix tests * Switch to contextmodel.ReqContext in plugins.go Co-authored-by: Will Browne <will.browne@grafana.com> Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
This commit is contained in:
@ -15,6 +15,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
@ -33,6 +35,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// pluginsCDNFallbackRedirectRequests is a metric counter keeping track of how many
|
||||
// requests are received on the plugins CDN backend redirect fallback handler.
|
||||
var pluginsCDNFallbackRedirectRequests = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "grafana",
|
||||
Name: "plugins_cdn_fallback_redirect_requests_total",
|
||||
Help: "Number of requests to the plugins CDN backend redirect fallback handler.",
|
||||
}, []string{"plugin_id", "plugin_version"})
|
||||
|
||||
func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Response {
|
||||
typeFilter := c.Query("type")
|
||||
enabledFilter := c.Query("enabled")
|
||||
@ -301,6 +311,13 @@ func (hs *HTTPServer) CollectPluginMetrics(c *contextmodel.ReqContext) response.
|
||||
|
||||
// getPluginAssets returns public plugin assets (images, JS, etc.)
|
||||
//
|
||||
// If the plugin has cdn = false in its config (default), it will always attempt to return the asset
|
||||
// from the local filesystem.
|
||||
//
|
||||
// If the plugin has cdn = true and hs.Cfg.PluginsCDNURLTemplate is empty, it will get the file
|
||||
// from the local filesystem. If hs.Cfg.PluginsCDNURLTemplate is not empty,
|
||||
// this handler returns a redirect to the plugin asset file on the specified CDN.
|
||||
//
|
||||
// /public/plugins/:pluginId/*
|
||||
func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
@ -318,7 +335,19 @@ func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := plugin.File(requestedFile)
|
||||
if hs.pluginsCDNService.PluginSupported(pluginID) {
|
||||
// Send a redirect to the client
|
||||
hs.redirectCDNPluginAsset(c, plugin, requestedFile)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the actual file to the client from local filesystem
|
||||
hs.serveLocalPluginAsset(c, plugin, requestedFile)
|
||||
}
|
||||
|
||||
// serveLocalPluginAsset returns the content of a plugin asset file from the local filesystem to the http client.
|
||||
func (hs *HTTPServer) serveLocalPluginAsset(c *contextmodel.ReqContext, plugin plugins.PluginDTO, assetPath string) {
|
||||
f, err := plugin.File(assetPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||
c.JsonApiErr(404, "Plugin file not found", nil)
|
||||
@ -346,15 +375,37 @@ func (hs *HTTPServer) getPluginAssets(c *contextmodel.ReqContext) {
|
||||
}
|
||||
|
||||
if rs, ok := f.(io.ReadSeeker); ok {
|
||||
http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), rs)
|
||||
} else {
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Plugin file exists but could not read", err)
|
||||
return
|
||||
}
|
||||
http.ServeContent(c.Resp, c.Req, requestedFile, fi.ModTime(), bytes.NewReader(b))
|
||||
http.ServeContent(c.Resp, c.Req, assetPath, fi.ModTime(), rs)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Plugin file exists but could not read", err)
|
||||
return
|
||||
}
|
||||
http.ServeContent(c.Resp, c.Req, assetPath, fi.ModTime(), bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// redirectCDNPluginAsset redirects the http request to specified asset path on the configured plugins CDN.
|
||||
func (hs *HTTPServer) redirectCDNPluginAsset(c *contextmodel.ReqContext, plugin plugins.PluginDTO, assetPath string) {
|
||||
remoteURL, err := hs.pluginsCDNService.AssetURL(plugin.ID, plugin.Info.Version, assetPath)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to get CDN plugin asset remote URL", err)
|
||||
return
|
||||
}
|
||||
hs.log.Warn(
|
||||
"plugin cdn redirect hit",
|
||||
"pluginID", plugin.ID,
|
||||
"pluginVersion", plugin.Info.Version,
|
||||
"assetPath", assetPath,
|
||||
"remoteURL", remoteURL,
|
||||
)
|
||||
pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{
|
||||
"plugin_id": plugin.ID,
|
||||
"plugin_version": plugin.Info.Version,
|
||||
}).Inc()
|
||||
http.Redirect(c.Resp, c.Req, remoteURL, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
// CheckHealth returns the health of a plugin.
|
||||
|
Reference in New Issue
Block a user