mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Previews: remove dashboard previews backend (#66176)
* remove dashboard previews backend * remove dashboard previews backend * bring back the migration * bring back the migration * bring back the migration
This commit is contained in:
@@ -479,12 +479,6 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
})
|
||||
})
|
||||
|
||||
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
|
||||
if hs.ThumbService != nil {
|
||||
dashUidRoute.Get("/img/:kind/:theme", hs.ThumbService.GetImage)
|
||||
}
|
||||
})
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
|
||||
dashboardRoute.Post("/validate", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.ValidateDashboard))
|
||||
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
|
||||
|
||||
@@ -64,16 +64,6 @@ type FrontendSettingsUnifiedAlertingDTO struct {
|
||||
MinInterval string `json:"minInterval"`
|
||||
}
|
||||
|
||||
type DashboardPreviewsSystemRequirements struct {
|
||||
Met bool `json:"met"`
|
||||
RequiredImageRendererPluginVersion string `json:"requiredImageRendererPluginVersion"`
|
||||
}
|
||||
|
||||
type DashboardPreviewsSetupConfig struct {
|
||||
SystemRequirements DashboardPreviewsSystemRequirements `json:"systemRequirements"`
|
||||
ThumbnailsExist bool `json:"thumbnailsExist"`
|
||||
}
|
||||
|
||||
// Enterprise-only
|
||||
type FrontendSettingsLicensingDTO struct {
|
||||
Slug *string `json:"slug,omitempty"`
|
||||
@@ -210,8 +200,6 @@ type FrontendSettingsDTO struct {
|
||||
SamlName string `json:"samlName"`
|
||||
TokenExpirationDayLimit int `json:"tokenExpirationDayLimit"`
|
||||
|
||||
DashboardPreviews DashboardPreviewsSetupConfig `json:"dashboardPreviews,omitempty"`
|
||||
|
||||
GeomapDefaultBaseLayerConfig *map[string]interface{} `json:"geomapDefaultBaseLayerConfig,omitempty"`
|
||||
GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"`
|
||||
|
||||
|
||||
@@ -233,10 +233,6 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
frontendSettings.PluginsCDNBaseURL = cdnBaseURL
|
||||
}
|
||||
|
||||
if hs.ThumbService != nil {
|
||||
frontendSettings.DashboardPreviews = hs.ThumbService.GetDashboardPreviewsSetupSettings(c)
|
||||
}
|
||||
|
||||
if hs.Cfg.GeomapDefaultBaseLayerConfig != nil {
|
||||
frontendSettings.GeomapDefaultBaseLayerConfig = &hs.Cfg.GeomapDefaultBaseLayerConfig
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
"github.com/grafana/grafana/pkg/services/teamguardian"
|
||||
tempUser "github.com/grafana/grafana/pkg/services/temp_user"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/validations"
|
||||
@@ -145,7 +144,6 @@ type HTTPServer struct {
|
||||
CorrelationsService correlations.Service
|
||||
Live *live.GrafanaLive
|
||||
LivePushGateway *pushhttp.Gateway
|
||||
ThumbService thumbs.Service
|
||||
StorageService store.StorageService
|
||||
httpEntityStore httpentitystore.HTTPEntityStore
|
||||
SearchV2HTTPService searchV2.SearchHTTPService
|
||||
@@ -198,20 +196,19 @@ type HTTPServer struct {
|
||||
kvStore kvstore.KVStore
|
||||
pluginsCDNService *pluginscdn.Service
|
||||
|
||||
userService user.Service
|
||||
tempUserService tempUser.Service
|
||||
dashboardThumbsService thumbs.DashboardThumbService
|
||||
loginAttemptService loginAttempt.Service
|
||||
orgService org.Service
|
||||
teamService team.Service
|
||||
accesscontrolService accesscontrol.Service
|
||||
annotationsRepo annotations.Repository
|
||||
tagService tag.Service
|
||||
oauthTokenService oauthtoken.OAuthTokenService
|
||||
statsService stats.Service
|
||||
authnService authn.Service
|
||||
starApi *starApi.API
|
||||
cachingService caching.CachingService
|
||||
userService user.Service
|
||||
tempUserService tempUser.Service
|
||||
loginAttemptService loginAttempt.Service
|
||||
orgService org.Service
|
||||
teamService team.Service
|
||||
accesscontrolService accesscontrol.Service
|
||||
annotationsRepo annotations.Repository
|
||||
tagService tag.Service
|
||||
oauthTokenService oauthtoken.OAuthTokenService
|
||||
statsService stats.Service
|
||||
authnService authn.Service
|
||||
starApi *starApi.API
|
||||
cachingService caching.CachingService
|
||||
}
|
||||
|
||||
type ServerOptions struct {
|
||||
@@ -225,8 +222,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
pluginDashboardService plugindashboards.Service, pluginStore plugins.Store, pluginClient plugins.Client,
|
||||
pluginErrorResolver plugins.ErrorResolver, pluginInstaller plugins.Installer, settingsProvider setting.Provider,
|
||||
dataSourceCache datasources.CacheService, userTokenService auth.UserTokenService,
|
||||
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service, correlationsService correlations.Service,
|
||||
thumbService thumbs.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
|
||||
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service, queryHistoryService queryhistory.Service, correlationsService correlations.Service, remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
|
||||
loginService login.Service, authenticator loginpkg.Authenticator, accessControl accesscontrol.AccessControl,
|
||||
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
|
||||
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
|
||||
@@ -251,7 +247,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore,
|
||||
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service,
|
||||
loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service,
|
||||
accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service,
|
||||
accesscontrolService accesscontrol.Service, navTreeService navtree.Service,
|
||||
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service,
|
||||
starApi *starApi.API, cachingService caching.CachingService,
|
||||
@@ -288,7 +284,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
QueryHistoryService: queryHistoryService,
|
||||
CorrelationsService: correlationsService,
|
||||
Features: features,
|
||||
ThumbService: thumbService,
|
||||
StorageService: storageService,
|
||||
RemoteCacheService: remoteCache,
|
||||
ProvisioningService: provisioningService,
|
||||
@@ -348,7 +343,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
PublicDashboardsApi: publicDashboardsApi,
|
||||
userService: userService,
|
||||
tempUserService: tempUserService,
|
||||
dashboardThumbsService: dashboardThumbsService,
|
||||
loginAttemptService: loginAttemptService,
|
||||
orgService: orgService,
|
||||
teamService: teamService,
|
||||
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/store/sanitizer"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
)
|
||||
|
||||
@@ -46,8 +45,7 @@ func ProvideBackgroundServiceRegistry(
|
||||
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, usageStats *uss.UsageStats,
|
||||
statsCollector *statscollector.Service, grafanaUpdateChecker *updatechecker.GrafanaService,
|
||||
pluginsUpdateChecker *updatechecker.PluginsService, metrics *metrics.InternalMetricsService,
|
||||
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache,
|
||||
thumbnailsService thumbs.Service, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
|
||||
secretsService *secretsManager.SecretsService, remoteCache *remotecache.RemoteCache, StorageService store.StorageService, searchService searchV2.SearchService, entityEventsService store.EntityEventsService,
|
||||
saService *samanager.ServiceAccountsService, authInfoService *authinfoservice.Implementation,
|
||||
grpcServerProvider grpcserver.Provider, secretMigrationProvider secretsMigrations.SecretMigrationProvider, loginAttemptService *loginattemptimpl.Service,
|
||||
bundleService *supportbundlesimpl.Service,
|
||||
@@ -78,7 +76,6 @@ func ProvideBackgroundServiceRegistry(
|
||||
remoteCache,
|
||||
secretsService,
|
||||
StorageService,
|
||||
thumbnailsService,
|
||||
searchService,
|
||||
entityEventsService,
|
||||
grpcServerProvider,
|
||||
|
||||
@@ -3,15 +3,12 @@ package usagestatssvcs
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
)
|
||||
|
||||
func ProvideUsageStatsProvidersRegistry(
|
||||
thumbsService thumbs.Service,
|
||||
accesscontrol accesscontrol.Service,
|
||||
) *UsageStatsProvidersRegistry {
|
||||
return NewUsageStatsProvidersRegistry(
|
||||
thumbsService,
|
||||
accesscontrol,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -136,8 +136,6 @@ import (
|
||||
teamguardianManager "github.com/grafana/grafana/pkg/services/teamguardian/manager"
|
||||
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
|
||||
"github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs/dashboardthumbsimpl"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -176,7 +174,6 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(query.Service), new(*query.ServiceImpl)),
|
||||
bus.ProvideBus,
|
||||
wire.Bind(new(bus.Bus), new(*bus.InProcBus)),
|
||||
thumbs.ProvideService,
|
||||
rendering.ProvideService,
|
||||
wire.Bind(new(rendering.Service), new(*rendering.RenderingService)),
|
||||
routing.ProvideRegister,
|
||||
@@ -186,7 +183,6 @@ var wireBasicSet = wire.NewSet(
|
||||
localcache.ProvideService,
|
||||
bundleregistry.ProvideService,
|
||||
wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)),
|
||||
dashboardthumbsimpl.ProvideService,
|
||||
updatechecker.ProvideGrafanaService,
|
||||
updatechecker.ProvidePluginsService,
|
||||
uss.ProvideService,
|
||||
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/validations"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -56,8 +55,6 @@ var wireExtsBasicSet = wire.NewSet(
|
||||
wire.Bind(new(accesscontrol.RoleRegistry), new(*acimpl.Service)),
|
||||
wire.Bind(new(plugins.RoleRegistry), new(*acimpl.Service)),
|
||||
wire.Bind(new(accesscontrol.Service), new(*acimpl.Service)),
|
||||
thumbs.ProvideCrawlerAuthSetupService,
|
||||
wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)),
|
||||
validations.ProvideValidator,
|
||||
wire.Bind(new(validations.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)),
|
||||
provisioning.ProvideService,
|
||||
|
||||
@@ -116,11 +116,6 @@ var (
|
||||
StatusCode: 404,
|
||||
Status: "not-found",
|
||||
}
|
||||
ErrDashboardThumbnailNotFound = DashboardErr{
|
||||
Reason: "Dashboard thumbnail not found",
|
||||
StatusCode: 404,
|
||||
Status: "not-found",
|
||||
}
|
||||
|
||||
ErrFolderNotFound = errors.New("folder not found")
|
||||
ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else")
|
||||
|
||||
@@ -27,12 +27,6 @@ var (
|
||||
State: FeatureStateStable,
|
||||
Owner: hostedGrafanaTeam,
|
||||
},
|
||||
{
|
||||
Name: "dashboardPreviews",
|
||||
Description: "Create and show thumbnails for dashboard search results",
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "live-service-web-worker",
|
||||
Description: "This will use a webworker thread to processes events rather than the main thread",
|
||||
|
||||
@@ -2,7 +2,6 @@ Name,State,Owner,requiresDevMode,RequiresLicense,RequiresRestart,FrontendOnly
|
||||
trimDefaults,beta,@grafana/grafana-as-code,false,false,false,false
|
||||
disableEnvelopeEncryption,stable,@grafana/grafana-as-code,false,false,false,false
|
||||
database_metrics,stable,@grafana/hosted-grafana-team,false,false,false,false
|
||||
dashboardPreviews,alpha,@grafana/grafana-app-platform-squad,false,false,false,false
|
||||
live-service-web-worker,alpha,@grafana/grafana-app-platform-squad,false,false,false,true
|
||||
queryOverLive,alpha,@grafana/grafana-app-platform-squad,false,false,false,true
|
||||
panelTitleSearch,beta,@grafana/grafana-app-platform-squad,false,false,false,false
|
||||
|
||||
|
@@ -19,10 +19,6 @@ const (
|
||||
// Add Prometheus metrics for database tables
|
||||
FlagDatabaseMetrics = "database_metrics"
|
||||
|
||||
// FlagDashboardPreviews
|
||||
// Create and show thumbnails for dashboard search results
|
||||
FlagDashboardPreviews = "dashboardPreviews"
|
||||
|
||||
// FlagLiveServiceWebWorker
|
||||
// This will use a webworker thread to processes events rather than the main thread
|
||||
FlagLiveServiceWebWorker = "live-service-web-worker"
|
||||
|
||||
@@ -11,7 +11,6 @@ func TestFeatureUsageStats(t *testing.T) {
|
||||
featureManagerWithAllFeatures := WithFeatures(
|
||||
"trimDefaults",
|
||||
"database_metrics",
|
||||
"dashboardPreviews",
|
||||
"live-config",
|
||||
"UPPER_SNAKE_CASE",
|
||||
"feature.with.a.dot",
|
||||
@@ -20,7 +19,6 @@ func TestFeatureUsageStats(t *testing.T) {
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"stats.features.trim_defaults.count": 1,
|
||||
"stats.features.database_metrics.count": 1,
|
||||
"stats.features.dashboard_previews.count": 1,
|
||||
"stats.features.live_config.count": 1,
|
||||
"stats.features.upper_snake_case.count": 1,
|
||||
"stats.features.feature_with_a_dot.count": 1,
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
package migrations
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
// This migration was run behind the `dashboardPreviews` flag.
|
||||
// The feature flag and the whole dashboard previews feature was removed in https://github.com/grafana/grafana/pull/66176
|
||||
|
||||
func addDashboardThumbsMigrations(mg *migrator.Migrator) {
|
||||
dashThumbs := migrator.Table{
|
||||
Name: "dashboard_thumbnail",
|
||||
Columns: []*migrator.Column{
|
||||
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "dashboard_id", Type: migrator.DB_BigInt, Nullable: false}, // can join with dashboard table
|
||||
{Name: "dashboard_version", Type: migrator.DB_Int, Nullable: false}, // screenshoted version of the dashboard
|
||||
{Name: "state", Type: migrator.DB_NVarchar, Length: 10, Nullable: false}, // stale | locked
|
||||
{Name: "panel_id", Type: migrator.DB_SmallInt, Nullable: false, Default: "0"}, // for panel thumbnails
|
||||
{Name: "image", Type: migrator.DB_MediumBlob, Nullable: false}, // image stored as blob. MediumBlob has a max limit of 16mb in MySQL
|
||||
{Name: "mime_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, // e.g. image/png, image/webp
|
||||
{Name: "kind", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // thumb | tall
|
||||
{Name: "theme", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // light|dark
|
||||
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
||||
},
|
||||
Indices: []*migrator.Index{
|
||||
{Cols: []string{"dashboard_id", "panel_id", "kind", "theme"}, Type: migrator.UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create dashboard_thumbnail table", migrator.NewAddTableMigration(dashThumbs))
|
||||
mg.AddMigration("add unique indexes for dashboard_thumbnail", migrator.NewAddIndexMigration(dashThumbs, dashThumbs.Indices[0]))
|
||||
mg.AddMigration("Add ds_uids column to dashboard_thumbnail table", migrator.NewAddColumnMigration(dashThumbs,
|
||||
// uids of datasources used in the dashboard when taking preview
|
||||
&migrator.Column{Name: "ds_uids", Type: migrator.DB_Text, Nullable: true, Default: ""},
|
||||
))
|
||||
}
|
||||
//func addDashboardThumbsMigrations(mg *migrator.Migrator) {
|
||||
// dashThumbs := migrator.Table{
|
||||
// Name: "dashboard_thumbnail",
|
||||
// Columns: []*migrator.Column{
|
||||
// {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
// {Name: "dashboard_id", Type: migrator.DB_BigInt, Nullable: false}, // can join with dashboard table
|
||||
// {Name: "dashboard_version", Type: migrator.DB_Int, Nullable: false}, // screenshoted version of the dashboard
|
||||
// {Name: "state", Type: migrator.DB_NVarchar, Length: 10, Nullable: false}, // stale | locked
|
||||
// {Name: "panel_id", Type: migrator.DB_SmallInt, Nullable: false, Default: "0"}, // for panel thumbnails
|
||||
// {Name: "image", Type: migrator.DB_MediumBlob, Nullable: false}, // image stored as blob. MediumBlob has a max limit of 16mb in MySQL
|
||||
// {Name: "mime_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: false}, // e.g. image/png, image/webp
|
||||
// {Name: "kind", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // thumb | tall
|
||||
// {Name: "theme", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, // light|dark
|
||||
// {Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
|
||||
// },
|
||||
// Indices: []*migrator.Index{
|
||||
// {Cols: []string{"dashboard_id", "panel_id", "kind", "theme"}, Type: migrator.UniqueIndex},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// mg.AddMigration("create dashboard_thumbnail table", migrator.NewAddTableMigration(dashThumbs))
|
||||
// mg.AddMigration("add unique indexes for dashboard_thumbnail", migrator.NewAddIndexMigration(dashThumbs, dashThumbs.Indices[0]))
|
||||
// mg.AddMigration("Add ds_uids column to dashboard_thumbnail table", migrator.NewAddColumnMigration(dashThumbs,
|
||||
// // uids of datasources used in the dashboard when taking preview
|
||||
// &migrator.Column{Name: "ds_uids", Type: migrator.DB_Text, Nullable: true, Default: ""},
|
||||
// ))
|
||||
//}
|
||||
|
||||
@@ -53,11 +53,6 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
|
||||
ualert.AddTablesMigrations(mg)
|
||||
ualert.AddDashAlertMigration(mg)
|
||||
addLibraryElementsMigrations(mg)
|
||||
if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil {
|
||||
if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagDashboardPreviews) {
|
||||
addDashboardThumbsMigrations(mg)
|
||||
}
|
||||
}
|
||||
|
||||
ualert.RerunDashAlertMigration(mg)
|
||||
addSecretsMigration(mg)
|
||||
|
||||
@@ -535,7 +535,6 @@ type InitTestDBOpt struct {
|
||||
}
|
||||
|
||||
var featuresEnabledDuringTests = []string{
|
||||
featuremgmt.FlagDashboardPreviews,
|
||||
featuremgmt.FlagPanelTitleSearch,
|
||||
featuremgmt.FlagEntityStore,
|
||||
}
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type simpleCrawler struct {
|
||||
renderService rendering.Service
|
||||
threadCount int
|
||||
concurrentLimit int
|
||||
renderingTimeout time.Duration
|
||||
|
||||
glive *live.GrafanaLive
|
||||
thumbnailRepo thumbnailRepo
|
||||
mode CrawlerMode
|
||||
thumbnailKind ThumbnailKind
|
||||
auth CrawlerAuth
|
||||
opts rendering.Opts
|
||||
status crawlStatus
|
||||
statusMutex sync.RWMutex
|
||||
queue []*DashboardWithStaleThumbnail
|
||||
queueMutex sync.Mutex
|
||||
log log.Logger
|
||||
renderingSessionByOrgId map[int64]rendering.Session
|
||||
dsUidsLookup getDatasourceUidsForDashboard
|
||||
}
|
||||
|
||||
func newSimpleCrawler(renderService rendering.Service, gl *live.GrafanaLive, repo thumbnailRepo, cfg *setting.Cfg, settings setting.DashboardPreviewsSettings, dsUidsLookup getDatasourceUidsForDashboard) dashRenderer {
|
||||
threadCount := int(settings.CrawlThreadCount)
|
||||
c := &simpleCrawler{
|
||||
// temporarily increases the concurrentLimit from the 'cfg.RendererConcurrentRequestLimit' to 'cfg.RendererConcurrentRequestLimit + crawlerThreadCount'
|
||||
concurrentLimit: cfg.RendererConcurrentRequestLimit + threadCount,
|
||||
renderingTimeout: settings.RenderingTimeout,
|
||||
renderService: renderService,
|
||||
threadCount: threadCount,
|
||||
glive: gl,
|
||||
dsUidsLookup: dsUidsLookup,
|
||||
thumbnailRepo: repo,
|
||||
log: log.New("thumbnails_crawler"),
|
||||
status: crawlStatus{
|
||||
State: initializing,
|
||||
Complete: 0,
|
||||
Queue: 0,
|
||||
},
|
||||
renderingSessionByOrgId: make(map[int64]rendering.Session),
|
||||
queue: nil,
|
||||
}
|
||||
c.broadcastStatus()
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) next(ctx context.Context) (*DashboardWithStaleThumbnail, rendering.Session, rendering.AuthOpts, error) {
|
||||
r.queueMutex.Lock()
|
||||
defer r.queueMutex.Unlock()
|
||||
|
||||
if r.queue == nil || len(r.queue) < 1 {
|
||||
return nil, nil, rendering.AuthOpts{}, nil
|
||||
}
|
||||
|
||||
v := r.queue[0]
|
||||
r.queue = r.queue[1:]
|
||||
|
||||
authOpts := rendering.AuthOpts{
|
||||
OrgID: v.OrgId,
|
||||
UserID: r.auth.GetUserId(v.OrgId),
|
||||
OrgRole: r.auth.GetOrgRole(),
|
||||
}
|
||||
|
||||
if renderingSession, ok := r.renderingSessionByOrgId[v.OrgId]; ok {
|
||||
return v, renderingSession, authOpts, nil
|
||||
}
|
||||
|
||||
renderingSession, err := r.renderService.CreateRenderingSession(ctx, authOpts, rendering.SessionOpts{
|
||||
Expiry: 5 * time.Minute,
|
||||
RefreshExpiryOnEachRequest: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, authOpts, err
|
||||
}
|
||||
|
||||
r.renderingSessionByOrgId[v.OrgId] = renderingSession
|
||||
return v, renderingSession, authOpts, nil
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) broadcastStatus() {
|
||||
s, err := r.Status()
|
||||
if err != nil {
|
||||
r.log.Warn("Error reading status", "err", err)
|
||||
return
|
||||
}
|
||||
msg, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
r.log.Warn("Error making message", "err", err)
|
||||
return
|
||||
}
|
||||
err = r.glive.Publish(r.opts.OrgID, "grafana/broadcast/crawler", msg)
|
||||
if err != nil {
|
||||
r.log.Warn("Error Publish message", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type byOrgId []*DashboardWithStaleThumbnail
|
||||
|
||||
func (d byOrgId) Len() int { return len(d) }
|
||||
func (d byOrgId) Less(i, j int) bool { return d[i].OrgId > d[j].OrgId }
|
||||
func (d byOrgId) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|
||||
|
||||
func (r *simpleCrawler) Run(ctx context.Context, auth CrawlerAuth, mode CrawlerMode, theme models.Theme, thumbnailKind ThumbnailKind) error {
|
||||
res, err := r.renderService.HasCapability(ctx, rendering.ScalingDownImages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !res.IsSupported {
|
||||
return fmt.Errorf("cant run dashboard crawler - rendering service needs to be updated. "+
|
||||
"current version: %s, requiredVersion: %s", r.renderService.Version(), res.SemverConstraint)
|
||||
}
|
||||
|
||||
runStarted := time.Now()
|
||||
|
||||
r.queueMutex.Lock()
|
||||
if r.IsRunning() {
|
||||
r.queueMutex.Unlock()
|
||||
r.log.Info("Already running")
|
||||
return nil
|
||||
}
|
||||
|
||||
items, err := r.thumbnailRepo.findDashboardsWithStaleThumbnails(ctx, theme, thumbnailKind)
|
||||
if err != nil {
|
||||
r.log.Error("Error when fetching dashboards with stale thumbnails", "err", err.Error())
|
||||
r.queueMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
r.queueMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// sort the items so that we render all items from each org before moving on to the next one
|
||||
// helps us avoid having to maintain multiple active rendering sessions
|
||||
sort.Sort(byOrgId(items))
|
||||
|
||||
r.mode = mode
|
||||
r.thumbnailKind = thumbnailKind
|
||||
r.auth = auth
|
||||
r.opts = rendering.Opts{
|
||||
TimeoutOpts: rendering.TimeoutOpts{
|
||||
Timeout: r.renderingTimeout,
|
||||
RequestTimeoutMultiplier: 3,
|
||||
},
|
||||
Theme: theme,
|
||||
ConcurrentLimit: r.concurrentLimit,
|
||||
}
|
||||
|
||||
r.renderingSessionByOrgId = make(map[int64]rendering.Session)
|
||||
r.queue = items
|
||||
r.status = crawlStatus{
|
||||
Started: runStarted,
|
||||
State: running,
|
||||
Complete: 0,
|
||||
}
|
||||
r.broadcastStatus()
|
||||
r.queueMutex.Unlock()
|
||||
|
||||
r.log.Info("Starting dashboard crawler", "threadCount", r.threadCount, "dashboardsToCrawl", len(items), "mode", string(mode), "theme", string(theme), "kind", string(thumbnailKind), "crawlerSetupTime", time.Since(runStarted))
|
||||
|
||||
group, gCtx := errgroup.WithContext(ctx)
|
||||
// create a pool of workers
|
||||
for i := 0; i < r.threadCount; i++ {
|
||||
walkerId := i
|
||||
group.Go(func() error {
|
||||
r.walk(gCtx, walkerId)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err = group.Wait()
|
||||
|
||||
status, _ := r.Status()
|
||||
r.log.Info("Crawl finished", "completedCount", status.Complete, "errorCount", status.Errors, "threadCount", r.threadCount, "dashboardsToCrawl", len(items), "mode", string(mode), "theme", string(theme), "kind", string(thumbnailKind), "crawlerRunTime", time.Since(runStarted))
|
||||
if err != nil {
|
||||
r.log.Error("Crawl ended with an error", "err", err)
|
||||
}
|
||||
|
||||
r.crawlFinished()
|
||||
r.broadcastStatus()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) IsRunning() bool {
|
||||
r.statusMutex.Lock()
|
||||
defer r.statusMutex.Unlock()
|
||||
return r.status.State == running
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) Stop() (crawlStatus, error) {
|
||||
r.statusMutex.Lock()
|
||||
if r.status.State == running {
|
||||
r.status.State = stopping
|
||||
}
|
||||
r.statusMutex.Unlock()
|
||||
|
||||
return r.Status()
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) Status() (crawlStatus, error) {
|
||||
r.statusMutex.RLock()
|
||||
defer r.statusMutex.RUnlock()
|
||||
|
||||
status := crawlStatus{
|
||||
State: r.status.State,
|
||||
Started: r.status.Started,
|
||||
Complete: r.status.Complete,
|
||||
Errors: r.status.Errors,
|
||||
Queue: len(r.queue),
|
||||
Last: r.status.Last,
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) newErrorResult() {
|
||||
r.statusMutex.Lock()
|
||||
defer r.statusMutex.Unlock()
|
||||
|
||||
r.status.Errors++
|
||||
r.status.Last = time.Now()
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) newSuccessResult() {
|
||||
r.statusMutex.Lock()
|
||||
defer r.statusMutex.Unlock()
|
||||
|
||||
r.status.Complete++
|
||||
r.status.Last = time.Now()
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) crawlFinished() {
|
||||
r.statusMutex.Lock()
|
||||
defer r.statusMutex.Unlock()
|
||||
|
||||
r.status.State = stopped
|
||||
r.status.Finished = time.Now()
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) shouldWalk() bool {
|
||||
r.statusMutex.RLock()
|
||||
defer r.statusMutex.RUnlock()
|
||||
|
||||
return r.status.State == running
|
||||
}
|
||||
|
||||
func (r *simpleCrawler) walk(ctx context.Context, id int) {
|
||||
walkerStarted := time.Now()
|
||||
|
||||
for {
|
||||
if !r.shouldWalk() {
|
||||
break
|
||||
}
|
||||
|
||||
itemStarted := time.Now()
|
||||
item, renderingSession, authOpts, err := r.next(ctx)
|
||||
if err != nil {
|
||||
r.log.Error("Render item retrieval error", "walkerId", id, "error", err)
|
||||
break
|
||||
}
|
||||
|
||||
if item == nil || renderingSession == nil {
|
||||
break
|
||||
}
|
||||
|
||||
url := dashboards.GetKioskModeDashboardURL(item.Uid, item.Slug, r.opts.Theme)
|
||||
r.log.Info("Getting dashboard thumbnail", "walkerId", id, "dashboardUID", item.Uid, "url", url)
|
||||
|
||||
dsUids, err := r.dsUidsLookup(ctx, item.Uid, item.OrgId)
|
||||
if err != nil {
|
||||
r.log.Warn("Error getting datasource uids", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err)
|
||||
r.newErrorResult()
|
||||
continue
|
||||
}
|
||||
|
||||
res, err := r.renderService.Render(ctx, rendering.Opts{
|
||||
Width: 320,
|
||||
Height: 240,
|
||||
Path: strings.TrimPrefix(url, "/"),
|
||||
AuthOpts: authOpts,
|
||||
TimeoutOpts: r.opts.TimeoutOpts,
|
||||
ConcurrentLimit: r.opts.ConcurrentLimit,
|
||||
Theme: r.opts.Theme,
|
||||
DeviceScaleFactor: -5, // negative numbers will render larger and then scale down.
|
||||
}, renderingSession)
|
||||
if err != nil {
|
||||
r.log.Warn("Error getting image", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err)
|
||||
r.newErrorResult()
|
||||
} else if res.FilePath == "" {
|
||||
r.log.Warn("Error getting image... no response", "walkerId", id, "dashboardUID", item.Uid, "url", url)
|
||||
r.newErrorResult()
|
||||
} else if strings.Contains(res.FilePath, "public/img") {
|
||||
r.log.Warn("Error getting image... internal result", "walkerId", id, "dashboardUID", item.Uid, "url", url, "img", res.FilePath)
|
||||
// rendering service returned a static error image - we should not remove that file
|
||||
r.newErrorResult()
|
||||
} else {
|
||||
func() {
|
||||
defer func() {
|
||||
err := os.Remove(res.FilePath)
|
||||
if err != nil {
|
||||
r.log.Error("Failed to remove thumbnail temp file", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
thumbnailId, err := r.thumbnailRepo.saveFromFile(ctx, res.FilePath, DashboardThumbnailMeta{
|
||||
DashboardUID: item.Uid,
|
||||
OrgId: item.OrgId,
|
||||
Theme: r.opts.Theme,
|
||||
Kind: r.thumbnailKind,
|
||||
}, item.Version, dsUids)
|
||||
|
||||
if err != nil {
|
||||
r.log.Warn("Error saving image image", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err, "itemTime", time.Since(itemStarted))
|
||||
r.newErrorResult()
|
||||
} else {
|
||||
r.log.Info("Saved thumbnail", "walkerId", id, "dashboardUID", item.Uid, "url", url, "thumbnailId", thumbnailId, "itemTime", time.Since(itemStarted))
|
||||
r.newSuccessResult()
|
||||
}
|
||||
}()
|
||||
}
|
||||
r.broadcastStatus()
|
||||
}
|
||||
|
||||
r.log.Info("Walker finished", "walkerId", id, "walkerTime", time.Since(walkerStarted))
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
)
|
||||
|
||||
type CrawlerAuthSetupService interface {
|
||||
Setup(ctx context.Context) (CrawlerAuth, error)
|
||||
}
|
||||
|
||||
func ProvideCrawlerAuthSetupService(serviceAccounts serviceaccounts.Service,
|
||||
sqlStore db.DB, orgService org.Service) *OSSCrawlerAuthSetupService {
|
||||
return &OSSCrawlerAuthSetupService{
|
||||
serviceAccountNamePrefix: "dashboard-previews-crawler-org-",
|
||||
serviceAccounts: serviceAccounts,
|
||||
log: log.New("oss_crawler_account_setup_service"),
|
||||
sqlStore: sqlStore,
|
||||
orgService: orgService,
|
||||
}
|
||||
}
|
||||
|
||||
type OSSCrawlerAuthSetupService struct {
|
||||
log log.Logger
|
||||
serviceAccountNamePrefix string
|
||||
serviceAccounts serviceaccounts.Service
|
||||
sqlStore db.DB
|
||||
orgService org.Service
|
||||
}
|
||||
|
||||
type CrawlerAuth interface {
|
||||
GetUserId(orgId int64) int64
|
||||
GetLogin(orgId int64) string
|
||||
GetOrgRole() org.RoleType
|
||||
}
|
||||
|
||||
func (o *OSSCrawlerAuthSetupService) findAllOrgIds(ctx context.Context) ([]int64, error) {
|
||||
searchAllOrgsQuery := &org.SearchOrgsQuery{}
|
||||
result, err := o.orgService.Search(ctx, searchAllOrgsQuery)
|
||||
if err != nil {
|
||||
o.log.Error("Error when searching for orgs", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgIds := make([]int64, 0)
|
||||
for i := range result {
|
||||
orgIds = append(orgIds, result[i].ID)
|
||||
}
|
||||
|
||||
return orgIds, nil
|
||||
}
|
||||
|
||||
type crawlerAuth struct {
|
||||
accountIdByOrgId map[int64]int64
|
||||
loginByOrgId map[int64]string
|
||||
orgRole org.RoleType
|
||||
}
|
||||
|
||||
func (o *crawlerAuth) GetOrgRole() org.RoleType {
|
||||
return o.orgRole
|
||||
}
|
||||
|
||||
func (o *crawlerAuth) GetUserId(orgId int64) int64 {
|
||||
return o.accountIdByOrgId[orgId]
|
||||
}
|
||||
|
||||
func (o *crawlerAuth) GetLogin(orgId int64) string {
|
||||
return o.loginByOrgId[orgId]
|
||||
}
|
||||
|
||||
func (o *OSSCrawlerAuthSetupService) Setup(ctx context.Context) (CrawlerAuth, error) {
|
||||
orgIds, err := o.findAllOrgIds(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// userId:0 and RoleAdmin grants the crawler process permissions to view all dashboards in all folders & orgs
|
||||
// the process doesn't and shouldn't actually need to edit/modify any resources from the UI
|
||||
orgRole := org.RoleAdmin
|
||||
|
||||
accountIdByOrgId := make(map[int64]int64)
|
||||
loginByOrgId := make(map[int64]string)
|
||||
for _, orgId := range orgIds {
|
||||
o.log.Info("Creating account for org", "orgId", orgId)
|
||||
|
||||
serviceAccountNameOrg := o.serviceAccountNamePrefix + strconv.FormatInt(orgId, 10)
|
||||
|
||||
saForm := serviceaccounts.CreateServiceAccountForm{
|
||||
Name: serviceAccountNameOrg,
|
||||
Role: &orgRole,
|
||||
}
|
||||
|
||||
serviceAccount, err := o.serviceAccounts.CreateServiceAccount(ctx, orgId, &saForm)
|
||||
accountAlreadyExists := errors.Is(err, serviceaccounts.ErrServiceAccountAlreadyExists)
|
||||
|
||||
if !accountAlreadyExists && err != nil {
|
||||
o.log.Error("Failed to create the service account", "err", err, "accountName", serviceAccountNameOrg, "orgId", orgId)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var serviceAccountLogin string
|
||||
var serviceAccountId int64
|
||||
if accountAlreadyExists {
|
||||
id, err := o.serviceAccounts.RetrieveServiceAccountIdByName(ctx, orgId, serviceAccountNameOrg)
|
||||
if err != nil {
|
||||
o.log.Error("Failed to retrieve service account", "err", err, "accountName", serviceAccountNameOrg)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update org_role to make sure everything works properly if someone has changed the role since SA's original creation
|
||||
dto, err := o.serviceAccounts.UpdateServiceAccount(ctx, orgId, id, &serviceaccounts.UpdateServiceAccountForm{
|
||||
Name: &serviceAccountNameOrg,
|
||||
Role: &orgRole,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
o.log.Error("Failed to update service account's role", "err", err, "accountName", serviceAccountNameOrg)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceAccountLogin = dto.Login
|
||||
serviceAccountId = id
|
||||
} else {
|
||||
serviceAccountLogin = serviceAccount.Login
|
||||
serviceAccountId = serviceAccount.Id
|
||||
}
|
||||
|
||||
accountIdByOrgId[orgId] = serviceAccountId
|
||||
loginByOrgId[orgId] = serviceAccountLogin
|
||||
}
|
||||
|
||||
return &crawlerAuth{accountIdByOrgId: accountIdByOrgId, loginByOrgId: loginByOrgId, orgRole: orgRole}, nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type DashboardThumbService interface {
|
||||
GetThumbnail(ctx context.Context, query *GetDashboardThumbnailCommand) (*DashboardThumbnail, error)
|
||||
SaveThumbnail(ctx context.Context, cmd *SaveDashboardThumbnailCommand) (*DashboardThumbnail, error)
|
||||
UpdateThumbnailState(ctx context.Context, cmd *UpdateThumbnailStateCommand) error
|
||||
FindThumbnailCount(ctx context.Context, cmd *FindDashboardThumbnailCountCommand) (int64, error)
|
||||
FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *FindDashboardsWithStaleThumbnailsCommand) ([]*DashboardWithStaleThumbnail, error)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package dashboardthumbsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
store store
|
||||
}
|
||||
|
||||
func ProvideService(db db.DB) thumbs.DashboardThumbService {
|
||||
return &Service{
|
||||
store: &xormStore{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetThumbnail(ctx context.Context, query *thumbs.GetDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error) {
|
||||
dt, err := s.store.Get(ctx, query)
|
||||
return dt, err
|
||||
}
|
||||
|
||||
func (s *Service) SaveThumbnail(ctx context.Context, cmd *thumbs.SaveDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error) {
|
||||
dt, err := s.store.Save(ctx, cmd)
|
||||
return dt, err
|
||||
}
|
||||
|
||||
func (s *Service) UpdateThumbnailState(ctx context.Context, cmd *thumbs.UpdateThumbnailStateCommand) error {
|
||||
err := s.store.UpdateState(ctx, cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) FindThumbnailCount(ctx context.Context, cmd *thumbs.FindDashboardThumbnailCountCommand) (int64, error) {
|
||||
n, err := s.store.Count(ctx, cmd)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *Service) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *thumbs.FindDashboardsWithStaleThumbnailsCommand) ([]*thumbs.DashboardWithStaleThumbnail, error) {
|
||||
thumbs, err := s.store.FindDashboardsWithStaleThumbnails(ctx, cmd)
|
||||
return thumbs, err
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package dashboardthumbsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
)
|
||||
|
||||
type store interface {
|
||||
Get(ctx context.Context, query *thumbs.GetDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error)
|
||||
Save(ctx context.Context, cmd *thumbs.SaveDashboardThumbnailCommand) (*thumbs.DashboardThumbnail, error)
|
||||
UpdateState(ctx context.Context, cmd *thumbs.UpdateThumbnailStateCommand) error
|
||||
Count(ctx context.Context, cmd *thumbs.FindDashboardThumbnailCountCommand) (int64, error)
|
||||
FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *thumbs.FindDashboardsWithStaleThumbnailsCommand) ([]*thumbs.DashboardWithStaleThumbnail, error)
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
package dashboardthumbsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var theme = models.ThemeDark
|
||||
var kind = thumbs.ThumbnailKindDefault
|
||||
|
||||
func TestIntegrationSqlStorage(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
var sqlStore db.DB
|
||||
var store store
|
||||
var savedFolder *dashboards.Dashboard
|
||||
|
||||
setup := func() {
|
||||
sqlStore = db.InitTestDB(t)
|
||||
store = &xormStore{db: sqlStore}
|
||||
savedFolder = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
|
||||
}
|
||||
|
||||
t.Run("Should insert dashboard in default state", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
thumb := getThumbnail(t, store, dash.UID, dash.OrgID)
|
||||
|
||||
require.Positive(t, thumb.Id)
|
||||
require.Equal(t, thumbs.ThumbnailStateDefault, thumb.State)
|
||||
require.Equal(t, dash.Version, thumb.DashboardVersion)
|
||||
})
|
||||
|
||||
t.Run("Should be able to update the thumbnail", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
thumb := getThumbnail(t, store, dash.UID, dash.OrgID)
|
||||
|
||||
insertedThumbnailId := thumb.Id
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version+1)
|
||||
|
||||
updatedThumb := getThumbnail(t, store, dash.UID, dash.OrgID)
|
||||
require.Equal(t, insertedThumbnailId, updatedThumb.Id)
|
||||
require.Equal(t, dash.Version+1, updatedThumb.DashboardVersion)
|
||||
})
|
||||
|
||||
t.Run("Should return empty array if all dashboards have thumbnails", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("Should return dashboards with thumbnails with empty ds_uids array", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
IncludeThumbnailsWithEmptyDsUIDs: true,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, dash.ID, res[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should return dashboards with thumbnails marked as stale", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
updateThumbnailState(t, store, dash.UID, dash.OrgID, thumbs.ThumbnailStateStale)
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, dash.ID, res[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should not return dashboards with updated thumbnails that had been marked as stale", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
updateThumbnailState(t, store, dash.UID, dash.OrgID, thumbs.ThumbnailStateStale)
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("Should find dashboards without thumbnails", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, dash.ID, res[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should find dashboards with outdated thumbnails", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
|
||||
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
|
||||
"tags": "different-tag",
|
||||
})
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, dash.ID, res[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should not return dashboards with locked thumbnails even if they are outdated", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, dash.Version)
|
||||
updateThumbnailState(t, store, dash.UID, dash.OrgID, thumbs.ThumbnailStateLocked)
|
||||
|
||||
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
|
||||
"tags": "different-tag",
|
||||
})
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("Should not return dashboards with manually uploaded thumbnails by default", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, thumbs.DashboardVersionForManualThumbnailUpload)
|
||||
|
||||
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
|
||||
"tags": "different-tag",
|
||||
})
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("Should return dashboards with manually uploaded thumbnails if requested", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, thumbs.DashboardVersionForManualThumbnailUpload)
|
||||
|
||||
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
|
||||
"tags": "different-tag",
|
||||
})
|
||||
|
||||
cmd := thumbs.FindDashboardsWithStaleThumbnailsCommand{
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
IncludeManuallyUploadedThumbnails: true,
|
||||
}
|
||||
res, err := store.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, dash.ID, res[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should count all dashboard thumbnails", func(t *testing.T) {
|
||||
setup()
|
||||
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash.UID, dash.OrgID, 1)
|
||||
dash2 := insertTestDashboard(t, sqlStore, "test dash 23", 2, savedFolder.ID, false, "prod", "webapp")
|
||||
upsertTestDashboardThumbnail(t, store, dash2.UID, dash2.OrgID, 1)
|
||||
|
||||
updateTestDashboard(t, sqlStore, dash, map[string]interface{}{
|
||||
"tags": "different-tag",
|
||||
})
|
||||
|
||||
cmd := thumbs.FindDashboardThumbnailCountCommand{}
|
||||
res, err := store.Count(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, res, int64(2))
|
||||
})
|
||||
}
|
||||
|
||||
func getThumbnail(t *testing.T, store store, dashboardUID string, orgId int64) *thumbs.DashboardThumbnail {
|
||||
t.Helper()
|
||||
cmd := thumbs.GetDashboardThumbnailCommand{
|
||||
DashboardThumbnailMeta: thumbs.DashboardThumbnailMeta{
|
||||
DashboardUID: dashboardUID,
|
||||
OrgId: orgId,
|
||||
PanelID: 0,
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
},
|
||||
}
|
||||
|
||||
thumb, err := store.Get(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
return thumb
|
||||
}
|
||||
|
||||
func upsertTestDashboardThumbnail(t *testing.T, store store, dashboardUID string, orgId int64, dashboardVersion int) *thumbs.DashboardThumbnail {
|
||||
t.Helper()
|
||||
cmd := thumbs.SaveDashboardThumbnailCommand{
|
||||
DashboardThumbnailMeta: thumbs.DashboardThumbnailMeta{
|
||||
DashboardUID: dashboardUID,
|
||||
OrgId: orgId,
|
||||
PanelID: 0,
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
},
|
||||
DashboardVersion: dashboardVersion,
|
||||
Image: make([]byte, 0),
|
||||
MimeType: "image/png",
|
||||
}
|
||||
dash, err := store.Save(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, dash)
|
||||
|
||||
return dash
|
||||
}
|
||||
|
||||
func updateThumbnailState(t *testing.T, store store, dashboardUID string, orgId int64, state thumbs.ThumbnailState) {
|
||||
t.Helper()
|
||||
cmd := thumbs.UpdateThumbnailStateCommand{
|
||||
DashboardThumbnailMeta: thumbs.DashboardThumbnailMeta{
|
||||
DashboardUID: dashboardUID,
|
||||
OrgId: orgId,
|
||||
PanelID: 0,
|
||||
Kind: kind,
|
||||
Theme: theme,
|
||||
},
|
||||
State: state,
|
||||
}
|
||||
err := store.UpdateState(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func updateTestDashboard(t *testing.T, sqlStore db.DB, dashModel *dashboards.Dashboard, data map[string]interface{}) {
|
||||
t.Helper()
|
||||
|
||||
data["id"] = dashModel.ID
|
||||
|
||||
parentVersion := dashModel.Version
|
||||
|
||||
cmd := dashboards.SaveDashboardCommand{
|
||||
OrgID: dashModel.OrgID,
|
||||
Overwrite: true,
|
||||
Dashboard: simplejson.NewFromAny(data),
|
||||
}
|
||||
var dash *dashboards.Dashboard
|
||||
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
var existing dashboards.Dashboard
|
||||
dash = cmd.GetDashboardModel()
|
||||
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existing)
|
||||
require.NoError(t, err)
|
||||
require.True(t, dashWithIdExists)
|
||||
|
||||
if dash.Version != existing.Version {
|
||||
dash.SetVersion(existing.Version)
|
||||
dash.Version = existing.Version
|
||||
}
|
||||
|
||||
dash.SetVersion(dash.Version + 1)
|
||||
dash.Created = time.Now()
|
||||
dash.Updated = time.Now()
|
||||
dash.ID = dashModel.ID
|
||||
dash.UID = util.GenerateShortUID()
|
||||
|
||||
_, err = sess.MustCols("folder_id").ID(dash.ID).Update(dash)
|
||||
return err
|
||||
})
|
||||
|
||||
require.Nil(t, err)
|
||||
|
||||
err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
dashVersion := &dashver.DashboardVersion{
|
||||
DashboardID: dash.ID,
|
||||
ParentVersion: parentVersion,
|
||||
RestoredFrom: cmd.RestoredFrom,
|
||||
Version: dash.Version,
|
||||
Created: time.Now(),
|
||||
CreatedBy: dash.UpdatedBy,
|
||||
Message: cmd.Message,
|
||||
Data: dash.Data,
|
||||
}
|
||||
|
||||
if affectedRows, err := sess.Insert(dashVersion); err != nil {
|
||||
return err
|
||||
} else if affectedRows == 0 {
|
||||
return dashboards.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func insertTestDashboard(t *testing.T, sqlStore db.DB, title string, orgId int64,
|
||||
folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard {
|
||||
t.Helper()
|
||||
cmd := dashboards.SaveDashboardCommand{
|
||||
OrgID: orgId,
|
||||
FolderID: folderId,
|
||||
IsFolder: isFolder,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": title,
|
||||
"tags": tags,
|
||||
}),
|
||||
}
|
||||
|
||||
var dash *dashboards.Dashboard
|
||||
err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
dash = cmd.GetDashboardModel()
|
||||
dash.SetVersion(1)
|
||||
dash.Created = time.Now()
|
||||
dash.Updated = time.Now()
|
||||
dash.UID = util.GenerateShortUID()
|
||||
_, err := sess.Insert(dash)
|
||||
return err
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, dash)
|
||||
dash.Data.Set("id", dash.ID)
|
||||
dash.Data.Set("uid", dash.UID)
|
||||
|
||||
err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
||||
dashVersion := &dashver.DashboardVersion{
|
||||
DashboardID: dash.ID,
|
||||
ParentVersion: dash.Version,
|
||||
RestoredFrom: cmd.RestoredFrom,
|
||||
Version: dash.Version,
|
||||
Created: time.Now(),
|
||||
CreatedBy: dash.UpdatedBy,
|
||||
Message: cmd.Message,
|
||||
Data: dash.Data,
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
if affectedRows, err := sess.Insert(dashVersion); err != nil {
|
||||
return err
|
||||
} else if affectedRows == 0 {
|
||||
return dashboards.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return dash
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
package dashboardthumbsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
)
|
||||
|
||||
type xormStore struct {
|
||||
db db.DB
|
||||
}
|
||||
|
||||
func (ss *xormStore) Get(ctx context.Context, query *thumbs.GetDashboardThumbnailCommand) (result *thumbs.DashboardThumbnail, err error) {
|
||||
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
thumb, err := findThumbnailByMeta(sess, query.DashboardThumbnailMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result = thumb
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func marshalDatasourceUids(dsUids []string) (string, error) {
|
||||
if dsUids == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(dsUids)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (ss *xormStore) Save(ctx context.Context, cmd *thumbs.SaveDashboardThumbnailCommand) (result *thumbs.DashboardThumbnail, err error) {
|
||||
err = ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
existing, err := findThumbnailByMeta(sess, cmd.DashboardThumbnailMeta)
|
||||
|
||||
if err != nil && !errors.Is(err, dashboards.ErrDashboardThumbnailNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
dsUids, err := marshalDatasourceUids(cmd.DatasourceUIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
existing.Image = cmd.Image
|
||||
existing.MimeType = cmd.MimeType
|
||||
existing.Updated = time.Now()
|
||||
existing.DashboardVersion = cmd.DashboardVersion
|
||||
existing.DsUIDs = dsUids
|
||||
existing.State = thumbs.ThumbnailStateDefault
|
||||
_, err = sess.ID(existing.Id).Update(existing)
|
||||
result = existing
|
||||
return err
|
||||
}
|
||||
|
||||
thumb := &thumbs.DashboardThumbnail{}
|
||||
|
||||
dash, err := findDashboardIdByThumbMeta(sess, cmd.DashboardThumbnailMeta)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
thumb.Updated = time.Now()
|
||||
thumb.Theme = cmd.Theme
|
||||
thumb.Kind = cmd.Kind
|
||||
thumb.Image = cmd.Image
|
||||
thumb.DsUIDs = dsUids
|
||||
thumb.MimeType = cmd.MimeType
|
||||
thumb.DashboardId = dash.Id
|
||||
thumb.DashboardVersion = cmd.DashboardVersion
|
||||
thumb.State = thumbs.ThumbnailStateDefault
|
||||
thumb.PanelId = cmd.PanelID
|
||||
_, err = sess.Insert(thumb)
|
||||
result = thumb
|
||||
return err
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (ss *xormStore) UpdateState(ctx context.Context, cmd *thumbs.UpdateThumbnailStateCommand) error {
|
||||
err := ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
existing, err := findThumbnailByMeta(sess, cmd.DashboardThumbnailMeta)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existing.State = cmd.State
|
||||
_, err = sess.ID(existing.Id).Update(existing)
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (ss *xormStore) Count(ctx context.Context, cmd *thumbs.FindDashboardThumbnailCountCommand) (n int64, err error) {
|
||||
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
count, err := sess.Count(&thumbs.DashboardThumbnail{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n = count
|
||||
return nil
|
||||
})
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (ss *xormStore) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *thumbs.FindDashboardsWithStaleThumbnailsCommand) (result []*thumbs.DashboardWithStaleThumbnail, err error) {
|
||||
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
sess.Table("dashboard")
|
||||
sess.Join("LEFT", "dashboard_thumbnail", "dashboard.id = dashboard_thumbnail.dashboard_id AND dashboard_thumbnail.theme = ? AND dashboard_thumbnail.kind = ?", cmd.Theme, cmd.Kind)
|
||||
sess.Where("dashboard.is_folder = ?", ss.db.GetDialect().BooleanStr(false))
|
||||
|
||||
query := "(dashboard.version != dashboard_thumbnail.dashboard_version " +
|
||||
"OR dashboard_thumbnail.state = ? " +
|
||||
"OR dashboard_thumbnail.id IS NULL"
|
||||
args := []interface{}{thumbs.ThumbnailStateStale}
|
||||
|
||||
if cmd.IncludeThumbnailsWithEmptyDsUIDs {
|
||||
query += " OR dashboard_thumbnail.ds_uids = ? OR dashboard_thumbnail.ds_uids IS NULL"
|
||||
args = append(args, "")
|
||||
}
|
||||
sess.Where(query+")", args...)
|
||||
|
||||
if !cmd.IncludeManuallyUploadedThumbnails {
|
||||
sess.Where("(dashboard_thumbnail.id is not null AND dashboard_thumbnail.dashboard_version != ?) "+
|
||||
"OR dashboard_thumbnail.id is null "+
|
||||
"OR dashboard_thumbnail.state = ?", thumbs.DashboardVersionForManualThumbnailUpload, thumbs.ThumbnailStateStale)
|
||||
}
|
||||
|
||||
sess.Where("(dashboard_thumbnail.id IS NULL OR dashboard_thumbnail.state != ?)", thumbs.ThumbnailStateLocked)
|
||||
|
||||
sess.Cols("dashboard.id",
|
||||
"dashboard.uid",
|
||||
"dashboard.org_id",
|
||||
"dashboard.version",
|
||||
"dashboard.slug")
|
||||
|
||||
var list = make([]*thumbs.DashboardWithStaleThumbnail, 0)
|
||||
err := sess.Find(&list)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result = list
|
||||
return err
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func findThumbnailByMeta(sess *db.Session, meta thumbs.DashboardThumbnailMeta) (*thumbs.DashboardThumbnail, error) {
|
||||
result := &thumbs.DashboardThumbnail{}
|
||||
|
||||
sess.Table("dashboard_thumbnail")
|
||||
sess.Join("INNER", "dashboard", "dashboard.id = dashboard_thumbnail.dashboard_id")
|
||||
sess.Where("dashboard.uid = ? AND dashboard.org_id = ? AND panel_id = ? AND kind = ? AND theme = ?", meta.DashboardUID, meta.OrgId, meta.PanelID, meta.Kind, meta.Theme)
|
||||
sess.Cols("dashboard_thumbnail.id",
|
||||
"dashboard_thumbnail.dashboard_id",
|
||||
"dashboard_thumbnail.panel_id",
|
||||
"dashboard_thumbnail.image",
|
||||
"dashboard_thumbnail.dashboard_version",
|
||||
"dashboard_thumbnail.state",
|
||||
"dashboard_thumbnail.kind",
|
||||
"dashboard_thumbnail.ds_uids",
|
||||
"dashboard_thumbnail.mime_type",
|
||||
"dashboard_thumbnail.theme",
|
||||
"dashboard_thumbnail.updated")
|
||||
exists, err := sess.Get(result)
|
||||
|
||||
if !exists {
|
||||
return nil, dashboards.ErrDashboardThumbnailNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type dash struct {
|
||||
Id int64
|
||||
}
|
||||
|
||||
func findDashboardIdByThumbMeta(sess *db.Session, meta thumbs.DashboardThumbnailMeta) (*dash, error) {
|
||||
result := &dash{}
|
||||
|
||||
sess.Table("dashboard").Where("dashboard.uid = ? AND dashboard.org_id = ?", meta.DashboardUID, meta.OrgId).Cols("id")
|
||||
exists, err := sess.Get(result)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, dashboards.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
||||
)
|
||||
|
||||
type getDatasourceUidsForDashboard func(ctx context.Context, dashboardUid string, orgId int64) ([]string, error)
|
||||
|
||||
type dsUidsLookup struct {
|
||||
searchService searchV2.SearchService
|
||||
crawlerAuth CrawlerAuth
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func getDatasourceUIDs(resp *backend.DataResponse, uid string) ([]string, error) {
|
||||
if resp == nil {
|
||||
return nil, errors.New("nil response")
|
||||
}
|
||||
|
||||
if resp.Error != nil {
|
||||
return nil, resp.Error
|
||||
}
|
||||
|
||||
if len(resp.Frames) == 0 {
|
||||
return nil, errors.New("empty response")
|
||||
}
|
||||
|
||||
frame := resp.Frames[0]
|
||||
field, idx := frame.FieldByName("ds_uid")
|
||||
|
||||
if field.Len() == 0 || idx == -1 {
|
||||
return nil, fmt.Errorf("no ds_uid field for uid %s", uid)
|
||||
}
|
||||
|
||||
rawValue, ok := field.At(0).(json.RawMessage)
|
||||
if !ok || rawValue == nil {
|
||||
return nil, fmt.Errorf("invalid value for uid %s in ds_uid field: %s", uid, field.At(0))
|
||||
}
|
||||
|
||||
jsonValue, err := rawValue.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var uids []string
|
||||
err = json.Unmarshal(jsonValue, &uids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return uids, nil
|
||||
}
|
||||
|
||||
func filterOutGrafanaDs(uids []string) []string {
|
||||
filtered := make([]string, 0)
|
||||
for _, uid := range uids {
|
||||
if uid != grafanads.DatasourceUID {
|
||||
filtered = append(filtered, uid)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (d *dsUidsLookup) getDatasourceUidsForDashboard(ctx context.Context, dashboardUid string, orgId int64) ([]string, error) {
|
||||
if d.searchService.IsDisabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
dashQueryResponse := d.searchService.DoDashboardQuery(ctx, &backend.User{
|
||||
Login: d.crawlerAuth.GetLogin(orgId),
|
||||
Role: string(d.crawlerAuth.GetOrgRole()),
|
||||
}, orgId, searchV2.DashboardQuery{
|
||||
UIDs: []string{dashboardUid},
|
||||
})
|
||||
|
||||
uids, err := getDatasourceUIDs(dashQueryResponse, dashboardUid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return filterOutGrafanaDs(uids), nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed testdata/search_response_frame.json
|
||||
exampleListFrameJSON string
|
||||
exampleListFrame = &data.Frame{}
|
||||
_ = exampleListFrame.UnmarshalJSON([]byte(exampleListFrameJSON))
|
||||
|
||||
//go:embed testdata/empty_search_response_frame.json
|
||||
listFrameJSONWithNoDatasources string
|
||||
listFrameWithNoDatasources = &data.Frame{}
|
||||
_ = listFrameWithNoDatasources.UnmarshalJSON([]byte(listFrameJSONWithNoDatasources))
|
||||
)
|
||||
|
||||
func TestShouldParseUidFromSearchResponseFrame(t *testing.T) {
|
||||
searchService := &searchV2.MockSearchService{}
|
||||
dsLookup := &dsUidsLookup{
|
||||
searchService: searchService,
|
||||
crawlerAuth: &crawlerAuth{},
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||
}
|
||||
|
||||
dashboardUid := "abc"
|
||||
searchService.On("IsDisabled").Return(false)
|
||||
searchService.On("DoDashboardQuery", mock.Anything, mock.Anything, mock.Anything, searchV2.DashboardQuery{
|
||||
UIDs: []string{dashboardUid},
|
||||
}).Return(&backend.DataResponse{
|
||||
Frames: []*data.Frame{exampleListFrame},
|
||||
})
|
||||
|
||||
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"datasource-2", "datasource-3", "datasource-4"}, uids)
|
||||
}
|
||||
|
||||
func TestShouldReturnEmptyArrayIfThereAreNoDatasources(t *testing.T) {
|
||||
searchService := &searchV2.MockSearchService{}
|
||||
dsLookup := &dsUidsLookup{
|
||||
searchService: searchService,
|
||||
crawlerAuth: &crawlerAuth{},
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||
}
|
||||
|
||||
dashboardUid := "abc"
|
||||
searchService.On("IsDisabled").Return(false)
|
||||
searchService.On("DoDashboardQuery", mock.Anything, mock.Anything, mock.Anything, searchV2.DashboardQuery{
|
||||
UIDs: []string{dashboardUid},
|
||||
}).Return(&backend.DataResponse{
|
||||
Frames: []*data.Frame{listFrameWithNoDatasources},
|
||||
})
|
||||
|
||||
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{}, uids)
|
||||
require.NotNil(t, uids)
|
||||
}
|
||||
|
||||
func TestShouldReturnNullIfSearchServiceIsDisabled(t *testing.T) {
|
||||
searchService := &searchV2.MockSearchService{}
|
||||
dsLookup := &dsUidsLookup{
|
||||
searchService: searchService,
|
||||
crawlerAuth: &crawlerAuth{},
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||
}
|
||||
|
||||
dashboardUid := "abc"
|
||||
searchService.On("IsDisabled").Return(true)
|
||||
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, uids)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
)
|
||||
|
||||
// When the feature flag is not enabled we just implement a dummy service
|
||||
type dummyService struct{}
|
||||
|
||||
func (ds *dummyService) GetUsageStats(ctx context.Context) map[string]interface{} {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func (ds *dummyService) GetImage(c *contextmodel.ReqContext) {
|
||||
c.JSON(400, map[string]string{"error": "invalid size"})
|
||||
}
|
||||
|
||||
func (ds *dummyService) UpdateThumbnailState(c *contextmodel.ReqContext) {
|
||||
c.JSON(400, map[string]string{"error": "invalid size"})
|
||||
}
|
||||
|
||||
func (ds *dummyService) SetImage(c *contextmodel.ReqContext) {
|
||||
c.JSON(400, map[string]string{"error": "invalid size"})
|
||||
}
|
||||
|
||||
func (ds *dummyService) Enabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ds *dummyService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dtos.DashboardPreviewsSetupConfig {
|
||||
return dtos.DashboardPreviewsSetupConfig{
|
||||
SystemRequirements: dtos.DashboardPreviewsSystemRequirements{
|
||||
Met: false,
|
||||
RequiredImageRendererPluginVersion: "",
|
||||
},
|
||||
ThumbnailsExist: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *dummyService) StartCrawler(c *contextmodel.ReqContext) response.Response {
|
||||
result := make(map[string]string)
|
||||
result["error"] = "Not enabled"
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (ds *dummyService) StopCrawler(c *contextmodel.ReqContext) response.Response {
|
||||
result := make(map[string]string)
|
||||
result["error"] = "Not enabled"
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (ds *dummyService) CrawlerStatus(c *contextmodel.ReqContext) response.Response {
|
||||
result := make(map[string]string)
|
||||
result["error"] = "Not enabled"
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (ds *dummyService) Run(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type CrawlerMode string
|
||||
|
||||
const (
|
||||
|
||||
// CrawlerModeThumbs will create small thumbnails for everything.
|
||||
CrawlerModeThumbs CrawlerMode = "thumbs"
|
||||
|
||||
// CrawlerModeAnalytics will get full page results for everything.
|
||||
CrawlerModeAnalytics CrawlerMode = "analytics"
|
||||
|
||||
// CrawlerModeMigrate will migrate all dashboards with old schema.
|
||||
CrawlerModeMigrate CrawlerMode = "migrate"
|
||||
)
|
||||
|
||||
type crawlerState string
|
||||
|
||||
const (
|
||||
initializing crawlerState = "initializing"
|
||||
running crawlerState = "running"
|
||||
stopping crawlerState = "stopping"
|
||||
stopped crawlerState = "stopped"
|
||||
)
|
||||
|
||||
type previewRequest struct {
|
||||
OrgID int64 `json:"orgId"`
|
||||
UID string `json:"uid"`
|
||||
Kind ThumbnailKind `json:"kind"`
|
||||
Theme models.Theme `json:"theme"`
|
||||
}
|
||||
|
||||
type crawlCmd struct {
|
||||
Mode CrawlerMode `json:"mode"` // thumbs | analytics | migrate
|
||||
Theme models.Theme `json:"theme"` // light | dark
|
||||
}
|
||||
|
||||
type crawlStatus struct {
|
||||
State crawlerState `json:"state"`
|
||||
Started time.Time `json:"started,omitempty"`
|
||||
Finished time.Time `json:"finished,omitempty"`
|
||||
Complete int `json:"complete"`
|
||||
Errors int `json:"errors"`
|
||||
Queue int `json:"queue"`
|
||||
Last time.Time `json:"last,omitempty"`
|
||||
}
|
||||
|
||||
type ThumbnailKind string
|
||||
type ThumbnailState string
|
||||
|
||||
const (
|
||||
// ThumbnailKindDefault is a small 320x240 preview
|
||||
ThumbnailKindDefault ThumbnailKind = "thumb"
|
||||
|
||||
// unsupported for now
|
||||
// - ThumbnailKindLarge ThumbnailKind = "large"
|
||||
// - ThumbnailKindTall ThumbnailKind = "tall"
|
||||
)
|
||||
|
||||
const (
|
||||
// ThumbnailStateDefault is the initial state for all thumbnails. Thumbnails in the "default" state will be considered stale,
|
||||
// and thus refreshed by the crawler, if the dashboard version from the time of taking the thumbnail is different from the current dashboard version
|
||||
ThumbnailStateDefault ThumbnailState = "default"
|
||||
|
||||
// ThumbnailStateStale is a manually assigned state. Thumbnails in the "stale" state will be refreshed on the next crawler run
|
||||
ThumbnailStateStale ThumbnailState = "stale"
|
||||
|
||||
// ThumbnailStateLocked is a manually assigned state. Thumbnails in the "locked" state will not be refreshed by the crawler as long as they remain in the "locked" state.
|
||||
ThumbnailStateLocked ThumbnailState = "locked"
|
||||
)
|
||||
|
||||
func (s ThumbnailState) IsValid() bool {
|
||||
return s == ThumbnailStateDefault || s == ThumbnailStateStale || s == ThumbnailStateLocked
|
||||
}
|
||||
|
||||
func (s *ThumbnailState) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
err := json.Unmarshal(data, &str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s = ThumbnailState(str)
|
||||
|
||||
if !s.IsValid() {
|
||||
if (*s) != "" {
|
||||
return fmt.Errorf("JSON validation error: invalid thumbnail state value: %s", *s)
|
||||
}
|
||||
|
||||
*s = ThumbnailStateDefault
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsKnownThumbnailKind checks if the value is supported
|
||||
func (p ThumbnailKind) IsKnownThumbnailKind() bool {
|
||||
switch p {
|
||||
case
|
||||
ThumbnailKindDefault:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ParseThumbnailKind(str string) (ThumbnailKind, error) {
|
||||
switch str {
|
||||
case string(ThumbnailKindDefault):
|
||||
return ThumbnailKindDefault, nil
|
||||
}
|
||||
return ThumbnailKindDefault, errors.New("unknown thumbnail kind " + str)
|
||||
}
|
||||
|
||||
// A DashboardThumbnail includes all metadata for a dashboard thumbnail
|
||||
type DashboardThumbnail struct {
|
||||
Id int64 `json:"id"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
DashboardVersion int `json:"dashboardVersion"`
|
||||
State ThumbnailState `json:"state"`
|
||||
PanelId int64 `json:"panelId,omitempty"`
|
||||
Kind ThumbnailKind `json:"kind"`
|
||||
Theme models.Theme `json:"theme"`
|
||||
Image []byte `json:"image"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Updated time.Time `json:"updated"`
|
||||
DsUIDs string `json:"-" xorm:"ds_uids"`
|
||||
}
|
||||
|
||||
//
|
||||
// Commands
|
||||
//
|
||||
|
||||
// DashboardThumbnailMeta uniquely identifies a thumbnail; a natural key
|
||||
type DashboardThumbnailMeta struct {
|
||||
DashboardUID string
|
||||
OrgId int64
|
||||
PanelID int64
|
||||
Kind ThumbnailKind
|
||||
Theme models.Theme
|
||||
}
|
||||
|
||||
type GetDashboardThumbnailCommand struct {
|
||||
DashboardThumbnailMeta
|
||||
}
|
||||
|
||||
const DashboardVersionForManualThumbnailUpload = -1
|
||||
|
||||
type DashboardWithStaleThumbnail struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
Uid string
|
||||
Version int
|
||||
Slug string
|
||||
}
|
||||
|
||||
type FindDashboardThumbnailCountCommand struct {
|
||||
}
|
||||
|
||||
type FindDashboardsWithStaleThumbnailsCommand struct {
|
||||
IncludeManuallyUploadedThumbnails bool
|
||||
IncludeThumbnailsWithEmptyDsUIDs bool
|
||||
Theme models.Theme
|
||||
Kind ThumbnailKind
|
||||
}
|
||||
|
||||
type SaveDashboardThumbnailCommand struct {
|
||||
DashboardThumbnailMeta
|
||||
DashboardVersion int
|
||||
Image []byte
|
||||
MimeType string
|
||||
DatasourceUIDs []string
|
||||
}
|
||||
|
||||
type UpdateThumbnailStateCommand struct {
|
||||
State ThumbnailState
|
||||
DashboardThumbnailMeta
|
||||
}
|
||||
|
||||
type dashRenderer interface {
|
||||
|
||||
// Run Assumes you have already authenticated as admin.
|
||||
Run(ctx context.Context, auth CrawlerAuth, mode CrawlerMode, theme models.Theme, kind ThumbnailKind) error
|
||||
|
||||
// Assumes you have already authenticated as admin.
|
||||
Stop() (crawlStatus, error)
|
||||
|
||||
// Assumes you have already authenticated as admin.
|
||||
Status() (crawlStatus, error)
|
||||
|
||||
IsRunning() bool
|
||||
}
|
||||
|
||||
type thumbnailRepo interface {
|
||||
updateThumbnailState(ctx context.Context, state ThumbnailState, meta DashboardThumbnailMeta) error
|
||||
doThumbnailsExist(ctx context.Context) (bool, error)
|
||||
saveFromFile(ctx context.Context, filePath string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error)
|
||||
saveFromBytes(ctx context.Context, bytes []byte, mimeType string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error)
|
||||
getThumbnail(ctx context.Context, meta DashboardThumbnailMeta) (*DashboardThumbnail, error)
|
||||
findDashboardsWithStaleThumbnails(ctx context.Context, theme models.Theme, thumbnailKind ThumbnailKind) ([]*DashboardWithStaleThumbnail, error)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
)
|
||||
|
||||
func newThumbnailRepo(thumbsService DashboardThumbService, search searchV2.SearchService) thumbnailRepo {
|
||||
repo := &sqlThumbnailRepository{
|
||||
store: thumbsService,
|
||||
search: search,
|
||||
log: log.New("thumbnails_repo"),
|
||||
}
|
||||
return repo
|
||||
}
|
||||
|
||||
type sqlThumbnailRepository struct {
|
||||
store DashboardThumbService
|
||||
search searchV2.SearchService
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (r *sqlThumbnailRepository) saveFromFile(ctx context.Context, filePath string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error) {
|
||||
// the filePath variable is never set by the user. it refers to a temporary file created either in
|
||||
// 1. thumbs/service.go, when user uploads a thumbnail
|
||||
// 2. the rendering service, when image-renderer returns a screenshot
|
||||
|
||||
if !filepath.IsAbs(filePath) {
|
||||
r.log.Error("Received relative path", "dashboardUID", meta.DashboardUID, "err", filePath)
|
||||
return 0, errors.New("relative paths are not supported")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Clean(filePath))
|
||||
|
||||
if err != nil {
|
||||
r.log.Error("error reading file", "dashboardUID", meta.DashboardUID, "err", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return r.saveFromBytes(ctx, content, getMimeType(filePath), meta, dashboardVersion, dsUids)
|
||||
}
|
||||
|
||||
func getMimeType(filePath string) string {
|
||||
if strings.HasSuffix(filePath, ".webp") {
|
||||
return "image/webp"
|
||||
}
|
||||
|
||||
return "image/png"
|
||||
}
|
||||
|
||||
func (r *sqlThumbnailRepository) saveFromBytes(ctx context.Context, content []byte, mimeType string, meta DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error) {
|
||||
cmd := &SaveDashboardThumbnailCommand{
|
||||
DashboardThumbnailMeta: meta,
|
||||
Image: content,
|
||||
MimeType: mimeType,
|
||||
DashboardVersion: dashboardVersion,
|
||||
DatasourceUIDs: dsUids,
|
||||
}
|
||||
|
||||
result, err := r.store.SaveThumbnail(ctx, cmd)
|
||||
if err != nil {
|
||||
r.log.Error("Error saving to the db", "dashboardUID", meta.DashboardUID, "err", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.Id, nil
|
||||
}
|
||||
|
||||
func (r *sqlThumbnailRepository) updateThumbnailState(ctx context.Context, state ThumbnailState, meta DashboardThumbnailMeta) error {
|
||||
return r.store.UpdateThumbnailState(ctx, &UpdateThumbnailStateCommand{
|
||||
State: state,
|
||||
DashboardThumbnailMeta: meta,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *sqlThumbnailRepository) getThumbnail(ctx context.Context, meta DashboardThumbnailMeta) (*DashboardThumbnail, error) {
|
||||
query := &GetDashboardThumbnailCommand{
|
||||
DashboardThumbnailMeta: meta,
|
||||
}
|
||||
return r.store.GetThumbnail(ctx, query)
|
||||
}
|
||||
|
||||
func (r *sqlThumbnailRepository) findDashboardsWithStaleThumbnails(ctx context.Context, theme models.Theme, kind ThumbnailKind) ([]*DashboardWithStaleThumbnail, error) {
|
||||
return r.store.FindDashboardsWithStaleThumbnails(ctx, &FindDashboardsWithStaleThumbnailsCommand{
|
||||
IncludeManuallyUploadedThumbnails: false,
|
||||
IncludeThumbnailsWithEmptyDsUIDs: !r.search.IsDisabled(),
|
||||
Theme: theme,
|
||||
Kind: kind,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *sqlThumbnailRepository) doThumbnailsExist(ctx context.Context) (bool, error) {
|
||||
cmd := &FindDashboardThumbnailCountCommand{}
|
||||
count, err := r.store.FindThumbnailCount(ctx, cmd)
|
||||
if err != nil {
|
||||
r.log.Error("Error finding thumbnails", "err", err)
|
||||
return false, err
|
||||
}
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
package thumbs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/permissions"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/live"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
registry.ProvidesUsageStats
|
||||
Run(ctx context.Context) error
|
||||
Enabled() bool
|
||||
GetImage(c *contextmodel.ReqContext)
|
||||
GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dtos.DashboardPreviewsSetupConfig
|
||||
|
||||
// from dashboard page
|
||||
SetImage(c *contextmodel.ReqContext) // form post
|
||||
UpdateThumbnailState(c *contextmodel.ReqContext)
|
||||
|
||||
// Must be admin
|
||||
StartCrawler(c *contextmodel.ReqContext) response.Response
|
||||
StopCrawler(c *contextmodel.ReqContext) response.Response
|
||||
CrawlerStatus(c *contextmodel.ReqContext) response.Response
|
||||
}
|
||||
|
||||
type thumbService struct {
|
||||
scheduleOptions crawlerScheduleOptions
|
||||
renderer dashRenderer
|
||||
renderingService rendering.Service
|
||||
thumbnailRepo thumbnailRepo
|
||||
lockService *serverlock.ServerLockService
|
||||
features featuremgmt.FeatureToggles
|
||||
store db.DB
|
||||
crawlLockServiceActionName string
|
||||
log log.Logger
|
||||
canRunCrawler bool
|
||||
settings setting.DashboardPreviewsSettings
|
||||
dashboardService dashboards.DashboardService
|
||||
dsUidsLookup getDatasourceUidsForDashboard
|
||||
dsPermissionsService permissions.DatasourcePermissionsService
|
||||
licensing licensing.Licensing
|
||||
searchService searchV2.SearchService
|
||||
}
|
||||
|
||||
type crawlerScheduleOptions struct {
|
||||
crawlInterval time.Duration
|
||||
tickerInterval time.Duration
|
||||
maxCrawlDuration time.Duration
|
||||
crawlerMode CrawlerMode
|
||||
thumbnailKind ThumbnailKind
|
||||
themes []models.Theme
|
||||
auth CrawlerAuth
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
lockService *serverlock.ServerLockService, renderService rendering.Service,
|
||||
gl *live.GrafanaLive, store db.DB, authSetupService CrawlerAuthSetupService,
|
||||
dashboardService dashboards.DashboardService, dashboardThumbsService DashboardThumbService, searchService searchV2.SearchService,
|
||||
dsPermissionsService permissions.DatasourcePermissionsService, licensing licensing.Licensing) Service {
|
||||
if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) {
|
||||
return &dummyService{}
|
||||
}
|
||||
logger := log.New("previews_service")
|
||||
|
||||
thumbnailRepo := newThumbnailRepo(dashboardThumbsService, searchService)
|
||||
|
||||
canRunCrawler := true
|
||||
|
||||
authSetupStarted := time.Now()
|
||||
crawlerAuth, err := authSetupService.Setup(context.Background())
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Crawler auth setup failed", "err", err, "crawlerAuthSetupTime", time.Since(authSetupStarted))
|
||||
canRunCrawler = false
|
||||
} else {
|
||||
logger.Info("Crawler auth setup complete", "crawlerAuthSetupTime", time.Since(authSetupStarted))
|
||||
}
|
||||
|
||||
dsUidsLookup := &dsUidsLookup{
|
||||
searchService: searchService,
|
||||
crawlerAuth: crawlerAuth,
|
||||
features: features,
|
||||
}
|
||||
|
||||
t := &thumbService{
|
||||
licensing: licensing,
|
||||
renderingService: renderService,
|
||||
renderer: newSimpleCrawler(renderService, gl, thumbnailRepo, cfg, cfg.DashboardPreviews, dsUidsLookup.getDatasourceUidsForDashboard),
|
||||
thumbnailRepo: thumbnailRepo,
|
||||
store: store,
|
||||
features: features,
|
||||
lockService: lockService,
|
||||
crawlLockServiceActionName: "dashboard-crawler",
|
||||
searchService: searchService,
|
||||
log: logger,
|
||||
canRunCrawler: canRunCrawler,
|
||||
dsUidsLookup: dsUidsLookup.getDatasourceUidsForDashboard,
|
||||
settings: cfg.DashboardPreviews,
|
||||
dsPermissionsService: dsPermissionsService,
|
||||
scheduleOptions: crawlerScheduleOptions{
|
||||
tickerInterval: 5 * time.Minute,
|
||||
crawlInterval: cfg.DashboardPreviews.SchedulerInterval,
|
||||
maxCrawlDuration: cfg.DashboardPreviews.MaxCrawlDuration,
|
||||
crawlerMode: CrawlerModeThumbs,
|
||||
thumbnailKind: ThumbnailKindDefault,
|
||||
themes: []models.Theme{models.ThemeDark, models.ThemeLight},
|
||||
auth: crawlerAuth,
|
||||
},
|
||||
dashboardService: dashboardService,
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (hs *thumbService) GetUsageStats(ctx context.Context) map[string]interface{} {
|
||||
s := hs.getDashboardPreviewsSetupSettings(ctx)
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
if s.SystemRequirements.Met {
|
||||
stats["stats.dashboard_previews.system_req_met.count"] = 1
|
||||
}
|
||||
|
||||
if s.ThumbnailsExist {
|
||||
stats["stats.dashboard_previews.thumbnails_exist.count"] = 1
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
func (hs *thumbService) Enabled() bool {
|
||||
return hs.features.IsEnabled(featuremgmt.FlagDashboardPreviews)
|
||||
}
|
||||
|
||||
func (hs *thumbService) parseImageReq(c *contextmodel.ReqContext, checkSave bool) *previewRequest {
|
||||
params := web.Params(c.Req)
|
||||
|
||||
kind, err := ParseThumbnailKind(params[":kind"])
|
||||
if err != nil {
|
||||
c.JSON(400, map[string]string{"error": "invalid size"})
|
||||
return nil
|
||||
}
|
||||
|
||||
theme, err := models.ParseTheme(params[":theme"])
|
||||
if err != nil {
|
||||
c.JSON(400, map[string]string{"error": "invalid theme"})
|
||||
return nil
|
||||
}
|
||||
|
||||
req := &previewRequest{
|
||||
OrgID: c.OrgID,
|
||||
UID: params[":uid"],
|
||||
Theme: theme,
|
||||
Kind: kind,
|
||||
}
|
||||
|
||||
if len(req.UID) < 1 {
|
||||
c.JSON(400, map[string]string{"error": "missing UID"})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check permissions and status
|
||||
status, err := hs.getStatus(c, req.UID, checkSave)
|
||||
if err != nil {
|
||||
c.JSON(status, map[string]string{"error": err.Error()})
|
||||
return nil
|
||||
}
|
||||
|
||||
if status != 200 {
|
||||
c.JSON(status, map[string]string{"error": fmt.Sprintf("code: %d", status)})
|
||||
return nil
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
type updateThumbnailStateRequest struct {
|
||||
State ThumbnailState `json:"state" binding:"Required"`
|
||||
}
|
||||
|
||||
func (hs *thumbService) UpdateThumbnailState(c *contextmodel.ReqContext) {
|
||||
req := hs.parseImageReq(c, false)
|
||||
if req == nil {
|
||||
return // already returned value
|
||||
}
|
||||
|
||||
var body = &updateThumbnailStateRequest{}
|
||||
|
||||
err := web.Bind(c.Req, body)
|
||||
if err != nil {
|
||||
hs.log.Error("Error parsing update thumbnail state request", "dashboardUid", req.UID, "err", err.Error())
|
||||
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return
|
||||
}
|
||||
|
||||
err = hs.thumbnailRepo.updateThumbnailState(c.Req.Context(), body.State, DashboardThumbnailMeta{
|
||||
DashboardUID: req.UID,
|
||||
OrgId: req.OrgID,
|
||||
Theme: req.Theme,
|
||||
Kind: ThumbnailKindDefault,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
hs.log.Error("Error when trying to update thumbnail state", "dashboardUid", req.UID, "err", err.Error(), "newState", body.State)
|
||||
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return
|
||||
}
|
||||
|
||||
hs.log.Info("Updated dashboard thumbnail state", "dashboardUid", req.UID, "theme", req.Theme, "newState", body.State)
|
||||
c.JSON(http.StatusOK, map[string]string{"success": "true"})
|
||||
}
|
||||
|
||||
func (hs *thumbService) GetImage(c *contextmodel.ReqContext) {
|
||||
req := hs.parseImageReq(c, false)
|
||||
if req == nil {
|
||||
return // already returned value
|
||||
}
|
||||
|
||||
res, err := hs.thumbnailRepo.getThumbnail(c.Req.Context(), DashboardThumbnailMeta{
|
||||
DashboardUID: req.UID,
|
||||
OrgId: req.OrgID,
|
||||
Theme: req.Theme,
|
||||
Kind: ThumbnailKindDefault,
|
||||
})
|
||||
|
||||
if errors.Is(err, dashboards.ErrDashboardThumbnailNotFound) {
|
||||
c.Resp.WriteHeader(404)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil || res == nil {
|
||||
hs.log.Error("Error when retrieving thumbnail", "dashboardUid", req.UID, "err", err.Error())
|
||||
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return
|
||||
}
|
||||
|
||||
if !hs.hasAccessToPreview(c, res, req) {
|
||||
return
|
||||
}
|
||||
|
||||
currentEtag := fmt.Sprintf("%d", res.Updated.Unix())
|
||||
c.Resp.Header().Set("ETag", currentEtag)
|
||||
|
||||
previousEtag := c.Req.Header.Get("If-None-Match")
|
||||
if previousEtag == currentEtag {
|
||||
c.Resp.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
c.Resp.Header().Set("Content-Type", res.MimeType)
|
||||
if _, err := c.Resp.Write(res.Image); err != nil {
|
||||
hs.log.Error("Error writing to response", "dashboardUid", req.UID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *thumbService) hasAccessToPreview(c *contextmodel.ReqContext, res *DashboardThumbnail, req *previewRequest) bool {
|
||||
if !hs.licensing.FeatureEnabled("accesscontrol.enforcement") {
|
||||
return true
|
||||
}
|
||||
|
||||
if hs.searchService.IsDisabled() {
|
||||
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return false
|
||||
}
|
||||
|
||||
if res.DsUIDs == "" {
|
||||
hs.log.Debug("dashboard preview is stale; no datasource uids", "dashboardUid", req.UID)
|
||||
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return false
|
||||
}
|
||||
|
||||
var dsUids []string
|
||||
err := json.Unmarshal([]byte(res.DsUIDs), &dsUids)
|
||||
|
||||
if err != nil {
|
||||
hs.log.Error("Error when retrieving datasource uids", "dashboardUid", req.UID, "err", err)
|
||||
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return false
|
||||
}
|
||||
|
||||
accessibleDatasources, err := hs.dsPermissionsService.FilterDatasourceUidsBasedOnQueryPermissions(c.Req.Context(), c.SignedInUser, dsUids)
|
||||
if err != nil && !errors.Is(err, permissions.ErrNotImplemented) {
|
||||
hs.log.Error("Error when filtering datasource uids", "dashboardUid", req.UID, "err", err)
|
||||
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return false
|
||||
}
|
||||
|
||||
if !errors.Is(err, permissions.ErrNotImplemented) {
|
||||
canQueryAllDatasources := len(accessibleDatasources) == len(dsUids)
|
||||
if !canQueryAllDatasources {
|
||||
hs.log.Info("Denied access to dashboard preview", "dashboardUid", req.UID, "err", err, "dashboardDatasources", dsUids, "accessibleDatasources", accessibleDatasources)
|
||||
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *contextmodel.ReqContext) dtos.DashboardPreviewsSetupConfig {
|
||||
return hs.getDashboardPreviewsSetupSettings(c.Req.Context())
|
||||
}
|
||||
|
||||
func (hs *thumbService) getDashboardPreviewsSetupSettings(ctx context.Context) dtos.DashboardPreviewsSetupConfig {
|
||||
systemRequirements := hs.getSystemRequirements(ctx)
|
||||
thumbnailsExist, err := hs.thumbnailRepo.doThumbnailsExist(ctx)
|
||||
|
||||
if err != nil {
|
||||
return dtos.DashboardPreviewsSetupConfig{
|
||||
SystemRequirements: systemRequirements,
|
||||
ThumbnailsExist: false,
|
||||
}
|
||||
}
|
||||
|
||||
return dtos.DashboardPreviewsSetupConfig{
|
||||
SystemRequirements: systemRequirements,
|
||||
ThumbnailsExist: thumbnailsExist,
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *thumbService) getSystemRequirements(ctx context.Context) dtos.DashboardPreviewsSystemRequirements {
|
||||
res, err := hs.renderingService.HasCapability(ctx, rendering.ScalingDownImages)
|
||||
if err != nil {
|
||||
hs.log.Error("Error when verifying dashboard previews system requirements thumbnail", "err", err.Error())
|
||||
return dtos.DashboardPreviewsSystemRequirements{
|
||||
Met: false,
|
||||
}
|
||||
}
|
||||
|
||||
if !res.IsSupported {
|
||||
return dtos.DashboardPreviewsSystemRequirements{
|
||||
Met: false,
|
||||
RequiredImageRendererPluginVersion: res.SemverConstraint,
|
||||
}
|
||||
}
|
||||
|
||||
return dtos.DashboardPreviewsSystemRequirements{
|
||||
Met: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Hack for now -- lets you upload images explicitly
|
||||
func (hs *thumbService) SetImage(c *contextmodel.ReqContext) {
|
||||
req := hs.parseImageReq(c, false)
|
||||
if req == nil {
|
||||
return // already returned value
|
||||
}
|
||||
|
||||
r := c.Req
|
||||
|
||||
// Parse our multipart form, 10 << 20 specifies a maximum
|
||||
// upload of 10 MB files.
|
||||
err := r.ParseMultipartForm(10 << 20)
|
||||
if err != nil {
|
||||
c.JSON(400, map[string]string{"error": "invalid upload size"})
|
||||
return
|
||||
}
|
||||
|
||||
// FormFile returns the first file for the given key `myFile`
|
||||
// it also returns the FileHeader so we can get the Filename,
|
||||
// the Header and the size of the file
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(400, map[string]string{"error": "missing multi-part form field named 'file'"})
|
||||
fmt.Println("error", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
hs.log.Info("Uploaded File: %+v\n", handler.Filename)
|
||||
hs.log.Info("File Size: %+v\n", handler.Size)
|
||||
hs.log.Info("MIME Header: %+v\n", handler.Header)
|
||||
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
c.JSON(400, map[string]string{"error": "error reading file"})
|
||||
return
|
||||
}
|
||||
|
||||
dsUids, err := hs.dsUidsLookup(c.Req.Context(), req.UID, req.OrgID)
|
||||
if err != nil {
|
||||
hs.log.Error("error looking up datasource ids", "err", err, "dashboardUid", req.UID)
|
||||
c.JSON(500, map[string]string{"error": "internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = hs.thumbnailRepo.saveFromBytes(c.Req.Context(), fileBytes, getMimeType(handler.Filename), DashboardThumbnailMeta{
|
||||
DashboardUID: req.UID,
|
||||
OrgId: req.OrgID,
|
||||
Theme: req.Theme,
|
||||
Kind: req.Kind,
|
||||
}, DashboardVersionForManualThumbnailUpload, dsUids)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, map[string]string{"error": "error saving thumbnail file"})
|
||||
fmt.Println("error", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, map[string]int{"OK": len(fileBytes)})
|
||||
}
|
||||
|
||||
func (hs *thumbService) StartCrawler(c *contextmodel.ReqContext) response.Response {
|
||||
body, err := io.ReadAll(c.Req.Body)
|
||||
if err != nil {
|
||||
return response.Error(500, "error reading bytes", err)
|
||||
}
|
||||
cmd := &crawlCmd{}
|
||||
err = json.Unmarshal(body, cmd)
|
||||
if err != nil {
|
||||
return response.Error(500, "error parsing bytes", err)
|
||||
}
|
||||
if cmd.Mode == "" {
|
||||
cmd.Mode = CrawlerModeThumbs
|
||||
}
|
||||
|
||||
go hs.runOnDemandCrawl(context.Background(), cmd.Theme, cmd.Mode, ThumbnailKindDefault, rendering.AuthOpts{
|
||||
OrgID: c.OrgID,
|
||||
UserID: c.UserID,
|
||||
OrgRole: c.OrgRole,
|
||||
})
|
||||
|
||||
status, err := hs.renderer.Status()
|
||||
if err != nil {
|
||||
return response.Error(500, "error starting", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
func (hs *thumbService) StopCrawler(c *contextmodel.ReqContext) response.Response {
|
||||
msg, err := hs.renderer.Stop()
|
||||
if err != nil {
|
||||
return response.Error(500, "error starting", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, msg)
|
||||
}
|
||||
|
||||
func (hs *thumbService) CrawlerStatus(c *contextmodel.ReqContext) response.Response {
|
||||
msg, err := hs.renderer.Status()
|
||||
if err != nil {
|
||||
return response.Error(500, "error starting", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, msg)
|
||||
}
|
||||
|
||||
// Ideally this service would not require first looking up the full dashboard just to bet the id!
|
||||
func (hs *thumbService) getStatus(c *contextmodel.ReqContext, uid string, checkSave bool) (int, error) {
|
||||
guardian, err := guardian.NewByUID(c.Req.Context(), uid, c.OrgID, c.SignedInUser)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if checkSave {
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
return 403, nil // forbidden
|
||||
}
|
||||
return 200, nil
|
||||
}
|
||||
|
||||
if canView, err := guardian.CanView(); err != nil || !canView {
|
||||
return 403, nil // forbidden
|
||||
}
|
||||
|
||||
return 200, nil // found and OK
|
||||
}
|
||||
|
||||
func (hs *thumbService) runOnDemandCrawl(parentCtx context.Context, theme models.Theme, mode CrawlerMode, kind ThumbnailKind, authOpts rendering.AuthOpts) {
|
||||
if !hs.canRunCrawler {
|
||||
return
|
||||
}
|
||||
|
||||
crawlerCtx, cancel := context.WithTimeout(parentCtx, hs.scheduleOptions.maxCrawlDuration)
|
||||
defer cancel()
|
||||
|
||||
// wait for at least a minute after the last completed run
|
||||
interval := time.Minute
|
||||
err := hs.lockService.LockAndExecute(crawlerCtx, hs.crawlLockServiceActionName, interval, func(ctx context.Context) {
|
||||
if err := hs.renderer.Run(crawlerCtx, hs.scheduleOptions.auth, mode, theme, kind); err != nil {
|
||||
hs.log.Error("On demand crawl error", "mode", mode, "theme", theme, "kind", kind, "userId", authOpts.UserID, "orgId", authOpts.OrgID, "orgRole", authOpts.OrgRole)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
hs.log.Error("On demand crawl lock error", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *thumbService) runScheduledCrawl(parentCtx context.Context) {
|
||||
crawlerCtx, cancel := context.WithTimeout(parentCtx, hs.scheduleOptions.maxCrawlDuration)
|
||||
defer cancel()
|
||||
|
||||
err := hs.lockService.LockAndExecute(crawlerCtx, hs.crawlLockServiceActionName, hs.scheduleOptions.crawlInterval, func(ctx context.Context) {
|
||||
for _, theme := range hs.scheduleOptions.themes {
|
||||
if err := hs.renderer.Run(crawlerCtx, hs.scheduleOptions.auth, hs.scheduleOptions.crawlerMode, theme, hs.scheduleOptions.thumbnailKind); err != nil {
|
||||
hs.log.Error("Scheduled crawl error", "theme", theme, "kind", hs.scheduleOptions.thumbnailKind, "err", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
hs.log.Error("Scheduled crawl lock error", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *thumbService) Run(ctx context.Context) error {
|
||||
if !hs.canRunCrawler {
|
||||
return nil
|
||||
}
|
||||
|
||||
gc := time.NewTicker(hs.scheduleOptions.tickerInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-gc.C:
|
||||
go hs.runScheduledCrawl(ctx)
|
||||
case <-ctx.Done():
|
||||
hs.log.Debug("Grafana is shutting down - stopping dashboard crawler")
|
||||
gc.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
{
|
||||
"schema": {
|
||||
"name": "Query results",
|
||||
"refId": "Search",
|
||||
"meta": {
|
||||
"type": "search-results",
|
||||
"custom": {
|
||||
"count": 106,
|
||||
"locationInfo": {
|
||||
"yboVMzb7z": {
|
||||
"name": "gdev dashboards",
|
||||
"kind": "folder",
|
||||
"url": "/dashboards/f/yboVMzb7z/gdev-dashboards"
|
||||
}
|
||||
},
|
||||
"sortBy": "name_sort"
|
||||
}
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "kind",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "uid",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "panel_type",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
},
|
||||
"config": {
|
||||
"links": [
|
||||
{
|
||||
"title": "link",
|
||||
"url": "${__value.text}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ds_uid",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
[
|
||||
"dashboard"
|
||||
],
|
||||
[
|
||||
"ujaM1h6nz"
|
||||
],
|
||||
[
|
||||
"abc2"
|
||||
],
|
||||
[
|
||||
""
|
||||
],
|
||||
[
|
||||
"/dashboards/f/ujaM1h6nz/abc2"
|
||||
],
|
||||
[
|
||||
[
|
||||
"gdev"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"grafana"
|
||||
]
|
||||
],
|
||||
[
|
||||
""
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
{
|
||||
"schema": {
|
||||
"name": "Query results",
|
||||
"refId": "Search",
|
||||
"meta": {
|
||||
"type": "search-results",
|
||||
"custom": {
|
||||
"count": 106,
|
||||
"locationInfo": {
|
||||
"yboVMzb7z": {
|
||||
"name": "gdev dashboards",
|
||||
"kind": "folder",
|
||||
"url": "/dashboards/f/yboVMzb7z/gdev-dashboards"
|
||||
}
|
||||
},
|
||||
"sortBy": "name_sort"
|
||||
}
|
||||
},
|
||||
"fields": [
|
||||
{
|
||||
"name": "kind",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "uid",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "panel_type",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
},
|
||||
"config": {
|
||||
"links": [
|
||||
{
|
||||
"title": "link",
|
||||
"url": "${__value.text}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ds_uid",
|
||||
"type": "other",
|
||||
"typeInfo": {
|
||||
"frame": "json.RawMessage",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"type": "string",
|
||||
"typeInfo": {
|
||||
"frame": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
[
|
||||
"dashboard"
|
||||
],
|
||||
[
|
||||
"ujaM1h6nz"
|
||||
],
|
||||
[
|
||||
"abc2"
|
||||
],
|
||||
[
|
||||
""
|
||||
],
|
||||
[
|
||||
"/dashboards/f/ujaM1h6nz/abc2"
|
||||
],
|
||||
[
|
||||
[
|
||||
"gdev"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"datasource-2",
|
||||
"datasource-3",
|
||||
"datasource-4",
|
||||
"grafana"
|
||||
]
|
||||
],
|
||||
[
|
||||
""
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -484,8 +484,6 @@ type Cfg struct {
|
||||
// Query history
|
||||
QueryHistoryEnabled bool
|
||||
|
||||
DashboardPreviews DashboardPreviewsSettings
|
||||
|
||||
Storage StorageSettings
|
||||
|
||||
Search SearchSettings
|
||||
@@ -1116,7 +1114,6 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
|
||||
|
||||
cfg.readDataSourcesSettings()
|
||||
|
||||
cfg.DashboardPreviews = readDashboardPreviewsSettings(iniFile)
|
||||
cfg.Storage = readStorageSettings(iniFile)
|
||||
cfg.Search = readSearchSettings(iniFile)
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type DashboardPreviewsSettings struct {
|
||||
SchedulerInterval time.Duration
|
||||
MaxCrawlDuration time.Duration
|
||||
RenderingTimeout time.Duration
|
||||
CrawlThreadCount uint32
|
||||
}
|
||||
|
||||
func readDashboardPreviewsSettings(iniFile *ini.File) DashboardPreviewsSettings {
|
||||
maxThreadCount := uint32(20)
|
||||
|
||||
s := DashboardPreviewsSettings{}
|
||||
|
||||
previewsCrawlerSection := iniFile.Section("dashboard_previews.crawler")
|
||||
s.CrawlThreadCount = uint32(previewsCrawlerSection.Key("thread_count").MustUint(6))
|
||||
if s.CrawlThreadCount > maxThreadCount {
|
||||
s.CrawlThreadCount = maxThreadCount
|
||||
}
|
||||
|
||||
s.SchedulerInterval = previewsCrawlerSection.Key("scheduler_interval").MustDuration(12 * time.Hour)
|
||||
s.MaxCrawlDuration = previewsCrawlerSection.Key("max_crawl_duration").MustDuration(1 * time.Hour)
|
||||
s.RenderingTimeout = previewsCrawlerSection.Key("rendering_timeout").MustDuration(20 * time.Second)
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user