mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2183 lines
67 KiB
2183 lines
67 KiB
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"go.opentelemetry.io/otel"
|
|
"golang.org/x/exp/slices"
|
|
"golang.org/x/sync/errgroup"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/selection"
|
|
k8sUser "k8s.io/apiserver/pkg/authentication/user"
|
|
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
|
|
"k8s.io/client-go/dynamic"
|
|
|
|
"github.com/grafana/authlib/claims"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
|
"github.com/grafana/grafana/pkg/infra/slugify"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/apiserver"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/services/guardian"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/quota"
|
|
"github.com/grafana/grafana/pkg/services/search/model"
|
|
"github.com/grafana/grafana/pkg/services/store/entity"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
var (
|
|
provisionerPermissions = []accesscontrol.Permission{
|
|
{Action: dashboards.ActionFoldersCreate, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: datasources.ActionRead, Scope: datasources.ScopeAll},
|
|
}
|
|
// DashboardServiceImpl implements the DashboardService interface
|
|
_ dashboards.DashboardService = (*DashboardServiceImpl)(nil)
|
|
_ dashboards.DashboardProvisioningService = (*DashboardServiceImpl)(nil)
|
|
_ dashboards.PluginService = (*DashboardServiceImpl)(nil)
|
|
|
|
daysInTrash = 24 * 30 * time.Hour
|
|
tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards/service")
|
|
)
|
|
|
|
type DashboardServiceImpl struct {
|
|
cfg *setting.Cfg
|
|
log log.Logger
|
|
dashboardStore dashboards.Store
|
|
folderStore folder.FolderStore
|
|
folderService folder.Service
|
|
userService user.Service
|
|
orgService org.Service
|
|
features featuremgmt.FeatureToggles
|
|
folderPermissions accesscontrol.FolderPermissionsService
|
|
dashboardPermissions accesscontrol.DashboardPermissionsService
|
|
ac accesscontrol.AccessControl
|
|
k8sclient dashboardK8sHandler
|
|
metrics *dashboardsMetrics
|
|
}
|
|
|
|
// interface to allow for testing
|
|
type dashboardK8sHandler interface {
|
|
getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
|
|
getNamespace(orgID int64) string
|
|
getSearcher() resource.ResourceIndexClient
|
|
}
|
|
|
|
var _ dashboardK8sHandler = (*dashk8sHandler)(nil)
|
|
|
|
type dashk8sHandler struct {
|
|
namespacer request.NamespaceMapper
|
|
gvr schema.GroupVersionResource
|
|
restConfigProvider apiserver.RestConfigProvider
|
|
searcher resource.ResourceIndexClient
|
|
}
|
|
|
|
// This is the uber service that implements a three smaller services
|
|
func ProvideDashboardServiceImpl(
|
|
cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore,
|
|
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
|
|
dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl,
|
|
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
|
|
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
|
|
quotaService quota.Service, orgService org.Service,
|
|
) (*DashboardServiceImpl, error) {
|
|
k8sHandler := &dashk8sHandler{
|
|
gvr: v0alpha1.DashboardResourceInfo.GroupVersionResource(),
|
|
namespacer: request.GetNamespaceMapper(cfg),
|
|
restConfigProvider: restConfigProvider,
|
|
searcher: unified,
|
|
}
|
|
|
|
dashSvc := &DashboardServiceImpl{
|
|
cfg: cfg,
|
|
log: log.New("dashboard-service"),
|
|
dashboardStore: dashboardStore,
|
|
features: features,
|
|
folderPermissions: folderPermissionsService,
|
|
dashboardPermissions: dashboardPermissionsService,
|
|
ac: ac,
|
|
folderStore: folderStore,
|
|
folderService: folderSvc,
|
|
orgService: orgService,
|
|
userService: userService,
|
|
k8sclient: k8sHandler,
|
|
metrics: newDashboardsMetrics(r),
|
|
}
|
|
|
|
defaultLimits, err := readQuotaConfig(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := quotaService.RegisterQuotaReporter("a.NewUsageReporter{
|
|
TargetSrv: dashboards.QuotaTargetSrv,
|
|
DefaultLimits: defaultLimits,
|
|
Reporter: dashSvc.Count,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, folderSvc))
|
|
ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, dashSvc, folderSvc))
|
|
|
|
if err := folderSvc.RegisterService(dashSvc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dashSvc, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
u := "a.Map{}
|
|
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
|
if err != nil {
|
|
return u, err
|
|
}
|
|
|
|
total := int64(0)
|
|
for _, org := range orgs {
|
|
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(org.ID))
|
|
dashs, err := dr.listDashboardsThroughK8s(ctx, org.ID)
|
|
if err != nil {
|
|
return u, err
|
|
}
|
|
orgDashboards := int64(len(dashs))
|
|
total += orgDashboards
|
|
|
|
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.Set(tag, orgDashboards)
|
|
}
|
|
|
|
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.Set(tag, total)
|
|
|
|
return u, nil
|
|
}
|
|
|
|
return dr.dashboardStore.Count(ctx, scopeParams)
|
|
}
|
|
|
|
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
|
limits := "a.Map{}
|
|
|
|
if cfg == nil {
|
|
return limits, nil
|
|
}
|
|
|
|
globalQuotaTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope)
|
|
if err != nil {
|
|
return "a.Map{}, err
|
|
}
|
|
orgQuotaTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope)
|
|
if err != nil {
|
|
return "a.Map{}, err
|
|
}
|
|
|
|
limits.Set(globalQuotaTag, cfg.Quota.Global.Dashboard)
|
|
limits.Set(orgQuotaTag, cfg.Quota.Org.Dashboard)
|
|
return limits, nil
|
|
}
|
|
|
|
func getDashboardBackgroundRequester(orgId int64) *identity.StaticRequester {
|
|
return &identity.StaticRequester{
|
|
Type: claims.TypeServiceAccount,
|
|
UserID: 1,
|
|
OrgID: orgId,
|
|
Name: "dashboard-background",
|
|
Login: "dashboard-background",
|
|
Permissions: map[int64]map[string][]string{
|
|
orgId: {
|
|
"*": {"*"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := []*dashboards.DashboardProvisioning{}
|
|
var mu sync.Mutex
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
for _, org := range orgs {
|
|
func(orgID int64) {
|
|
g.Go(func() error {
|
|
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
|
ProvisionedRepo: name,
|
|
OrgId: orgID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mu.Lock()
|
|
for _, r := range res {
|
|
results = append(results, &r.DashboardProvisioning)
|
|
}
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
}(org.ID)
|
|
}
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
return dr.dashboardStore.GetProvisionedDashboardData(ctx, name)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
// if dashboard id is 0, it is a new dashboard
|
|
if dashboardID == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, org := range orgs {
|
|
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
|
OrgId: org.ID,
|
|
DashboardIds: []int64{dashboardID},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(res) == 1 {
|
|
return &res[0].DashboardProvisioning, nil
|
|
} else if len(res) > 1 {
|
|
return nil, fmt.Errorf("found more than one provisioned dashboard with ID %d", dashboardID)
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
return dr.dashboardStore.GetProvisionedDataByDashboardID(ctx, dashboardID)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
if dashboardUID == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
|
OrgId: orgID,
|
|
DashboardUIDs: []string{dashboardUID},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(res) == 1 {
|
|
return &res[0].DashboardProvisioning, nil
|
|
} else if len(res) > 1 {
|
|
return nil, fmt.Errorf("found more than one provisioned dashboard with UID %s", dashboardUID)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
return dr.dashboardStore.GetProvisionedDataByDashboardUID(ctx, orgID, dashboardUID)
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
|
validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.BuildSaveDashboardcommand")
|
|
defer span.End()
|
|
|
|
dash := dto.Dashboard
|
|
|
|
dash.OrgID = dto.OrgID
|
|
dash.Title = strings.TrimSpace(dash.Title)
|
|
dash.Data.Set("title", dash.Title)
|
|
dash.SetUID(strings.TrimSpace(dash.UID))
|
|
|
|
if dash.Title == "" {
|
|
return nil, dashboards.ErrDashboardTitleEmpty
|
|
}
|
|
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
// nolint:staticcheck
|
|
if dash.IsFolder && dash.FolderID > 0 {
|
|
return nil, dashboards.ErrDashboardFolderCannotHaveParent
|
|
}
|
|
|
|
if dash.IsFolder && strings.EqualFold(dash.Title, dashboards.RootFolderName) {
|
|
return nil, dashboards.ErrDashboardFolderNameExists
|
|
}
|
|
|
|
if !util.IsValidShortUID(dash.UID) {
|
|
return nil, dashboards.ErrDashboardInvalidUid
|
|
} else if util.IsShortUIDTooLong(dash.UID) {
|
|
return nil, dashboards.ErrDashboardUidTooLong
|
|
}
|
|
|
|
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dash); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate folder
|
|
if dash.FolderUID != "" {
|
|
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
|
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
// nolint:staticcheck
|
|
dash.FolderID = folder.ID
|
|
}
|
|
} else if dash.FolderID != 0 { // nolint:staticcheck
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
// nolint:staticcheck
|
|
folder, err := dr.folderStore.GetFolderByID(ctx, dash.OrgID, dash.FolderID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dash.FolderUID = folder.UID
|
|
}
|
|
|
|
isParentFolderChanged, err := dr.ValidateDashboardBeforeSave(ctx, dash, dto.Overwrite)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isParentFolderChanged {
|
|
// Check that the user is allowed to add a dashboard to the folder
|
|
guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgID, dto.User)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
// nolint:staticcheck
|
|
if canSave, err := guardian.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canSave {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, dashboards.ErrDashboardUpdateAccessDenied
|
|
}
|
|
}
|
|
|
|
if validateProvisionedDashboard {
|
|
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if provisionedData != nil {
|
|
return nil, dashboards.ErrDashboardCannotSaveProvisionedDashboard
|
|
}
|
|
}
|
|
|
|
guard, err := getGuardianForSavePermissionCheck(ctx, dash, dto.User)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dash.ID == 0 {
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
// nolint:staticcheck
|
|
if canCreate, err := guard.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canCreate {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, dashboards.ErrDashboardUpdateAccessDenied
|
|
}
|
|
} else {
|
|
if canSave, err := guard.CanSave(); err != nil || !canSave {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, dashboards.ErrDashboardUpdateAccessDenied
|
|
}
|
|
}
|
|
|
|
var userID int64
|
|
if id, err := identity.UserIdentifier(dto.User.GetID()); err == nil {
|
|
userID = id
|
|
} else {
|
|
dr.log.Debug("User does not belong to a user or service account namespace, using 0 as user ID", "id", dto.User.GetID())
|
|
}
|
|
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
cmd := &dashboards.SaveDashboardCommand{
|
|
Dashboard: dash.Data,
|
|
Message: dto.Message,
|
|
OrgID: dto.OrgID,
|
|
Overwrite: dto.Overwrite,
|
|
UserID: userID,
|
|
FolderID: dash.FolderID, // nolint:staticcheck
|
|
FolderUID: dash.FolderUID,
|
|
IsFolder: dash.IsFolder,
|
|
PluginID: dash.PluginID,
|
|
}
|
|
|
|
if !dto.UpdatedAt.IsZero() {
|
|
cmd.UpdatedAt = dto.UpdatedAt
|
|
}
|
|
|
|
return cmd, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) ValidateDashboardBeforeSave(ctx context.Context, dashboard *dashboards.Dashboard, overwrite bool) (bool, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.ValidateDashboardBeforesave")
|
|
defer span.End()
|
|
|
|
isParentFolderChanged := false
|
|
|
|
var existingById *dashboards.Dashboard
|
|
var err error
|
|
if dashboard.ID > 0 {
|
|
// if ID is set and the dashboard is not found, ErrDashboardNotFound will be returned
|
|
existingById, err = dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: dashboard.OrgID, ID: dashboard.ID})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if dashboard.UID == "" {
|
|
dashboard.SetUID(existingById.UID)
|
|
}
|
|
}
|
|
dashWithIdExists := (existingById != nil)
|
|
|
|
var existingByUid *dashboards.Dashboard
|
|
if dashboard.UID != "" {
|
|
existingByUid, err = dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: dashboard.OrgID, UID: dashboard.UID})
|
|
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
|
|
return false, err
|
|
}
|
|
}
|
|
dashWithUidExists := (existingByUid != nil)
|
|
|
|
if !dashWithIdExists && !dashWithUidExists {
|
|
return false, nil
|
|
}
|
|
|
|
if dashWithIdExists && dashWithUidExists && existingById.ID != existingByUid.ID {
|
|
return false, dashboards.ErrDashboardWithSameUIDExists
|
|
}
|
|
|
|
existing := existingById
|
|
|
|
if !dashWithIdExists && dashWithUidExists {
|
|
dashboard.SetID(existingByUid.ID)
|
|
dashboard.SetUID(existingByUid.UID)
|
|
existing = existingByUid
|
|
}
|
|
|
|
if (existing.IsFolder && !dashboard.IsFolder) ||
|
|
(!existing.IsFolder && dashboard.IsFolder) {
|
|
return isParentFolderChanged, dashboards.ErrDashboardTypeMismatch
|
|
}
|
|
|
|
if !dashboard.IsFolder && dashboard.FolderUID != existing.FolderUID {
|
|
isParentFolderChanged = true
|
|
}
|
|
|
|
// check for is someone else has written in between
|
|
if dashboard.Version != existing.Version {
|
|
if overwrite {
|
|
dashboard.SetVersion(existing.Version)
|
|
} else {
|
|
return isParentFolderChanged, dashboards.ErrDashboardVersionMismatch
|
|
}
|
|
}
|
|
|
|
// do not allow plugin dashboard updates without overwrite flag
|
|
if existing.PluginID != "" && !overwrite {
|
|
return isParentFolderChanged, dashboards.UpdatePluginDashboardError{PluginId: existing.PluginID}
|
|
}
|
|
|
|
return isParentFolderChanged, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
// check each org for orphaned provisioned dashboards
|
|
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, org := range orgs {
|
|
// find all dashboards in the org that have a file repo set that is not in the given readers list
|
|
foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, dashboards.FindPersistedDashboardsQuery{
|
|
ProvisionedReposNotIn: cmd.ReaderNames,
|
|
OrgId: org.ID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// delete them
|
|
for _, foundDash := range foundDashs {
|
|
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(org.ID))
|
|
if err = dr.deleteDashboard(ctx, foundDash.DashboardID, foundDash.DashboardUID, org.ID, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd)
|
|
}
|
|
|
|
// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard
|
|
// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck()
|
|
func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user identity.Requester) (guardian.DashboardGuardian, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.getGuardianForSavePermissionCheck")
|
|
defer span.End()
|
|
|
|
newDashboard := d.ID == 0
|
|
|
|
if newDashboard {
|
|
// if it's a new dashboard/folder check the parent folder permissions
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
// nolint:staticcheck
|
|
guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return guard, nil
|
|
}
|
|
guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return guard, nil
|
|
}
|
|
|
|
func validateDashboardRefreshInterval(minRefreshInterval string, dash *dashboards.Dashboard) error {
|
|
if minRefreshInterval == "" {
|
|
return nil
|
|
}
|
|
|
|
refresh := dash.Data.Get("refresh").MustString("")
|
|
if refresh == "" || refresh == "auto" {
|
|
// since no refresh is set it is a valid refresh rate
|
|
return nil
|
|
}
|
|
|
|
minRefreshIntervalDur, err := gtime.ParseDuration(minRefreshInterval)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing min refresh interval %q failed: %w", minRefreshInterval, err)
|
|
}
|
|
d, err := gtime.ParseDuration(refresh)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing refresh duration %q failed: %w", refresh, err)
|
|
}
|
|
|
|
if d < minRefreshIntervalDur {
|
|
return dashboards.ErrDashboardRefreshIntervalTooShort
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
|
provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.SaveProvisionedDashboard")
|
|
defer span.End()
|
|
|
|
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
|
dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid",
|
|
dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", dr.cfg.MinRefreshInterval)
|
|
dto.Dashboard.Data.Set("refresh", dr.cfg.MinRefreshInterval)
|
|
}
|
|
|
|
dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions)
|
|
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(dto.OrgID))
|
|
|
|
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var dash *dashboards.Dashboard
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
// save the dashboard but then do NOT return
|
|
// we want to save the provisioning data to the dashboard_provisioning table still
|
|
// to ensure we can safely rollback to mode2 if needed
|
|
dash, err = dr.saveProvisionedDashboardThroughK8s(ctx, cmd, provisioning, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
dash, err = dr.saveDashboard(ctx, cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = dr.dashboardStore.SaveProvisionedDashboard(ctx, dash, provisioning)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if dto.Dashboard.ID == 0 {
|
|
dr.setDefaultPermissions(ctx, dto, dash, true)
|
|
}
|
|
|
|
return dash, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *folder.CreateFolderCommand) (*folder.Folder, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.SaveFolderForProvisionedDashboards")
|
|
defer span.End()
|
|
|
|
dto.SignedInUser = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions)
|
|
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(dto.OrgID))
|
|
|
|
f, err := dr.folderService.Create(ctx, dto)
|
|
if err != nil {
|
|
dr.log.Error("failed to create folder for provisioned dashboards", "folder", dto.Title, "org", dto.OrgID, "err", err)
|
|
return nil, err
|
|
}
|
|
|
|
dr.setDefaultFolderPermissions(ctx, dto, f, true)
|
|
return f, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
|
allowUiUpdate bool) (*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.SaveDashboard")
|
|
defer span.End()
|
|
|
|
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
|
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
|
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval",
|
|
dr.cfg.MinRefreshInterval)
|
|
dto.Dashboard.Data.Set("refresh", dr.cfg.MinRefreshInterval)
|
|
}
|
|
|
|
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, !allowUiUpdate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dash, err := dr.saveDashboard(ctx, cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// new dashboard created
|
|
if dto.Dashboard.ID == 0 {
|
|
dr.setDefaultPermissions(ctx, dto, dash, false)
|
|
}
|
|
|
|
return dash, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
return dr.saveDashboardThroughK8s(ctx, cmd, cmd.OrgID)
|
|
}
|
|
|
|
return dr.dashboardStore.SaveDashboard(ctx, *cmd)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
return dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: orgID, UID: uid, IncludeDeleted: true})
|
|
}
|
|
|
|
return dr.dashboardStore.GetSoftDeletedDashboard(ctx, orgID, uid)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) RestoreDashboard(ctx context.Context, dashboard *dashboards.Dashboard, user identity.Requester, optionalFolderUID string) error {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.RestoreDashboard")
|
|
defer span.End()
|
|
|
|
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
|
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
|
|
}
|
|
|
|
// if the optionalFolder is provided we need to check if the folder exists and user has access to it
|
|
if optionalFolderUID != "" {
|
|
restoringFolder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{
|
|
UID: &optionalFolderUID,
|
|
OrgID: dashboard.OrgID,
|
|
SignedInUser: user,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
|
return dashboards.ErrFolderRestoreNotFound
|
|
}
|
|
return folder.ErrInternal.Errorf("failed to fetch parent folder from store: %w", err)
|
|
}
|
|
|
|
return dr.dashboardStore.RestoreDashboard(ctx, dashboard.OrgID, dashboard.UID, restoringFolder)
|
|
}
|
|
|
|
// if the optionalFolder is not provided we need to restore the dashboard to the original folder
|
|
// we check for permissions and the folder existence before restoring
|
|
restoringFolder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{
|
|
UID: &dashboard.FolderUID,
|
|
OrgID: dashboard.OrgID,
|
|
SignedInUser: user,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
|
return dashboards.ErrFolderRestoreNotFound
|
|
}
|
|
return folder.ErrInternal.Errorf("failed to fetch parent folder from store: %w", err)
|
|
}
|
|
|
|
// TODO: once restore in k8s is finalized, add functionality here under the feature toggle
|
|
|
|
return dr.dashboardStore.RestoreDashboard(ctx, dashboard.OrgID, dashboard.UID, restoringFolder)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.SoftDeleteDashboard")
|
|
defer span.End()
|
|
|
|
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
|
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
|
|
}
|
|
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
// deletes in unistore are soft deletes, so we can just delete in the same way
|
|
return dr.deleteDashboardThroughK8s(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgID, UID: dashboardUID}, true)
|
|
}
|
|
|
|
provisionedData, _ := dr.GetProvisionedDashboardDataByDashboardUID(ctx, orgID, dashboardUID)
|
|
if provisionedData != nil && provisionedData.ID != 0 {
|
|
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
|
|
}
|
|
|
|
return dr.dashboardStore.SoftDeleteDashboard(ctx, orgID, dashboardUID)
|
|
}
|
|
|
|
// DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for
|
|
// operations by the user where we want to make sure user does not delete provisioned dashboard.
|
|
func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error {
|
|
return dr.deleteDashboard(ctx, dashboardId, dashboardUID, orgId, true)
|
|
}
|
|
|
|
// DeleteAllDashboards will delete all dashboards within a given org.
|
|
func (dr *DashboardServiceImpl) DeleteAllDashboards(ctx context.Context, orgId int64) error {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
return dr.deleteAllDashboardThroughK8s(ctx, orgId)
|
|
}
|
|
|
|
return dr.dashboardStore.DeleteAllDashboards(ctx, orgId)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*dashboards.Dashboard, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// DeleteProvisionedDashboard removes dashboard from the DB even if it is provisioned.
|
|
func (dr *DashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
|
|
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(orgId))
|
|
return dr.deleteDashboard(ctx, dashboardId, "", orgId, false)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64, validateProvisionedDashboard bool) error {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.deleteDashboard")
|
|
defer span.End()
|
|
|
|
cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID}
|
|
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
err := dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// cleanup things related to dashboards that are not stored in unistore yet
|
|
return dr.dashboardStore.CleanupAfterDelete(ctx, cmd)
|
|
}
|
|
|
|
if validateProvisionedDashboard {
|
|
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dashboardId)
|
|
if err != nil {
|
|
return fmt.Errorf("%v: %w", "failed to check if dashboard is provisioned", err)
|
|
}
|
|
|
|
if provisionedData != nil {
|
|
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
|
|
}
|
|
}
|
|
|
|
return dr.dashboardStore.DeleteDashboard(ctx, cmd)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (
|
|
*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.ImportDashboard")
|
|
defer span.End()
|
|
|
|
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
|
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
|
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title,
|
|
"minRefreshInterval", dr.cfg.MinRefreshInterval)
|
|
dto.Dashboard.Data.Set("refresh", dr.cfg.MinRefreshInterval)
|
|
}
|
|
|
|
cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dash, err := dr.saveDashboard(ctx, cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dr.setDefaultPermissions(ctx, dto, dash, false)
|
|
|
|
return dash, nil
|
|
}
|
|
|
|
// UnprovisionDashboard removes info about dashboard being provisioned. Used after provisioning configs are changed
|
|
// and provisioned dashboards are left behind but not deleted.
|
|
func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashboardId int64) error {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, org := range orgs {
|
|
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(org.ID))
|
|
dash, err := dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: org.ID, ID: dashboardId})
|
|
if err != nil {
|
|
// if we can't find it in this org, try the next one
|
|
continue
|
|
}
|
|
|
|
_, err = dr.saveProvisionedDashboardThroughK8s(ctx, &dashboards.SaveDashboardCommand{
|
|
OrgID: org.ID,
|
|
PluginID: dash.PluginID,
|
|
FolderUID: dash.FolderUID,
|
|
FolderID: dash.FolderID, // nolint:staticcheck
|
|
UpdatedAt: time.Now(),
|
|
Dashboard: dash.Data,
|
|
}, nil, true)
|
|
|
|
return err
|
|
}
|
|
|
|
return dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
return dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
|
OrgId: query.OrgID,
|
|
ProvisionedRepo: pluginIDRepoName,
|
|
ProvisionedPath: query.PluginID,
|
|
})
|
|
}
|
|
return dr.dashboardStore.GetDashboardsByPluginID(ctx, query)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultPermissions")
|
|
defer span.End()
|
|
|
|
resource := "dashboard"
|
|
if dash.IsFolder {
|
|
resource = "folder"
|
|
}
|
|
|
|
if !dr.cfg.RBAC.PermissionsOnCreation(resource) {
|
|
return
|
|
}
|
|
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
// nolint:staticcheck
|
|
inFolder := dash.FolderID > 0
|
|
var permissions []accesscontrol.SetResourcePermissionCommand
|
|
|
|
if !provisioned && dto.User.IsIdentityType(claims.TypeUser, claims.TypeServiceAccount) {
|
|
userID, err := dto.User.GetInternalID()
|
|
if err != nil {
|
|
dr.log.Error("Could not make user admin", "dashboard", dash.Title, "id", dto.User.GetID(), "error", err)
|
|
} else {
|
|
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
|
|
UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(),
|
|
})
|
|
}
|
|
}
|
|
|
|
if !inFolder {
|
|
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
|
|
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
|
|
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
|
|
}...)
|
|
}
|
|
|
|
svc := dr.dashboardPermissions
|
|
if dash.IsFolder {
|
|
svc = dr.folderPermissions
|
|
}
|
|
|
|
if _, err := svc.SetPermissions(ctx, dto.OrgID, dash.UID, permissions...); err != nil {
|
|
dr.log.Error("Could not set default permissions", "dashboard", dash.Title, "error", err)
|
|
}
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, cmd *folder.CreateFolderCommand, f *folder.Folder, provisioned bool) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultFolderPermissions")
|
|
defer span.End()
|
|
|
|
if !dr.cfg.RBAC.PermissionsOnCreation("folder") {
|
|
return
|
|
}
|
|
|
|
inFolder := f.ParentUID != ""
|
|
var permissions []accesscontrol.SetResourcePermissionCommand
|
|
|
|
if !provisioned && cmd.SignedInUser.IsIdentityType(claims.TypeUser) {
|
|
userID, err := cmd.SignedInUser.GetInternalID()
|
|
if err != nil {
|
|
dr.log.Error("Could not make user admin", "folder", cmd.Title, "id", cmd.SignedInUser.GetID())
|
|
} else {
|
|
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
|
|
UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(),
|
|
})
|
|
}
|
|
}
|
|
|
|
if !inFolder {
|
|
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
|
|
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
|
|
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
|
|
}...)
|
|
}
|
|
|
|
if _, err := dr.folderPermissions.SetPermissions(ctx, cmd.OrgID, f.UID, permissions...); err != nil {
|
|
dr.log.Error("Could not set default folder permissions", "folder", f.Title, "error", err)
|
|
}
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
return dr.getDashboardThroughK8s(ctx, query)
|
|
}
|
|
|
|
return dr.dashboardStore.GetDashboard(ctx, query)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
requester, err := identity.GetRequester(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
|
OrgId: requester.GetOrgID(),
|
|
DashboardIds: []int64{query.ID},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(result) != 1 {
|
|
return nil, fmt.Errorf("unexpected number of dashboards found: %d. desired: 1", len(result))
|
|
}
|
|
|
|
return &dashboards.DashboardRef{UID: result[0].UID, Slug: result[0].Slug}, nil
|
|
}
|
|
|
|
return dr.dashboardStore.GetDashboardUIDByID(ctx, query)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
if query.OrgID == 0 {
|
|
requester, err := identity.GetRequester(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.OrgID = requester.GetOrgID()
|
|
}
|
|
|
|
return dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
|
DashboardIds: query.DashboardIDs,
|
|
OrgId: query.OrgID,
|
|
DashboardUIDs: query.DashboardUIDs,
|
|
})
|
|
}
|
|
|
|
return dr.dashboardStore.GetDashboards(ctx, query)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.Dashboard, error) {
|
|
return dr.getDashboardsSharedWithUser(ctx, user)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.getDashboardsSharedWithUser")
|
|
defer span.End()
|
|
|
|
permissions := user.GetPermissions()
|
|
dashboardPermissions := permissions[dashboards.ActionDashboardsRead]
|
|
sharedDashboards := make([]*dashboards.Dashboard, 0)
|
|
dashboardUids := make([]string, 0)
|
|
for _, p := range dashboardPermissions {
|
|
if dashboardUid, found := strings.CutPrefix(p, dashboards.ScopeDashboardsPrefix); found {
|
|
if !slices.Contains(dashboardUids, dashboardUid) {
|
|
dashboardUids = append(dashboardUids, dashboardUid)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(dashboardUids) == 0 {
|
|
return sharedDashboards, nil
|
|
}
|
|
|
|
dashboardsQuery := &dashboards.GetDashboardsQuery{
|
|
DashboardUIDs: dashboardUids,
|
|
OrgID: user.GetOrgID(),
|
|
}
|
|
sharedDashboards, err := dr.GetDashboards(ctx, dashboardsQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dr.filterUserSharedDashboards(ctx, user, sharedDashboards)
|
|
}
|
|
|
|
// filterUserSharedDashboards filter dashboards directly assigned to user, but not located in folders with view permissions
|
|
func (dr *DashboardServiceImpl) filterUserSharedDashboards(ctx context.Context, user identity.Requester, userDashboards []*dashboards.Dashboard) ([]*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.filterUserSharedDashboards")
|
|
defer span.End()
|
|
|
|
filteredDashboards := make([]*dashboards.Dashboard, 0)
|
|
|
|
folderUIDs := make([]string, 0)
|
|
for _, dashboard := range userDashboards {
|
|
folderUIDs = append(folderUIDs, dashboard.FolderUID)
|
|
}
|
|
|
|
// GetFolders return only folders available to user. So we can use is to check access.
|
|
userDashFolders, err := dr.folderService.GetFolders(ctx, folder.GetFoldersQuery{
|
|
UIDs: folderUIDs,
|
|
OrgID: user.GetOrgID(),
|
|
OrderByTitle: true,
|
|
SignedInUser: user,
|
|
})
|
|
if err != nil {
|
|
return nil, folder.ErrInternal.Errorf("failed to fetch parent folders from store: %w", err)
|
|
}
|
|
|
|
dashFoldersMap := make(map[string]*folder.Folder, 0)
|
|
for _, f := range userDashFolders {
|
|
dashFoldersMap[f.UID] = f
|
|
}
|
|
|
|
for _, dashboard := range userDashboards {
|
|
// Filter out dashboards if user has access to parent folder
|
|
if dashboard.FolderUID == "" {
|
|
continue
|
|
}
|
|
|
|
_, hasAccess := dashFoldersMap[dashboard.FolderUID]
|
|
if !hasAccess {
|
|
filteredDashboards = append(filteredDashboards, dashboard)
|
|
}
|
|
}
|
|
return filteredDashboards, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context, user identity.Requester) ([]string, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.getUserSharedDashboardsUIDs")
|
|
defer span.End()
|
|
|
|
userDashboards, err := dr.getDashboardsSharedWithUser(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userDashboardUIDs := make([]string, 0)
|
|
for _, dashboard := range userDashboards {
|
|
userDashboardUIDs = append(userDashboardUIDs, dashboard.UID)
|
|
}
|
|
return userDashboardUIDs, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards")
|
|
defer span.End()
|
|
|
|
if dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) {
|
|
start := time.Now()
|
|
userDashboardUIDs, err := dr.getUserSharedDashboardUIDs(ctx, query.SignedInUser)
|
|
if err != nil {
|
|
dr.metrics.sharedWithMeFetchDashboardsRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
|
|
return nil, err
|
|
}
|
|
if len(userDashboardUIDs) == 0 {
|
|
return []dashboards.DashboardSearchProjection{}, nil
|
|
}
|
|
query.DashboardUIDs = userDashboardUIDs
|
|
query.FolderUIDs = []string{}
|
|
|
|
defer func(t time.Time) {
|
|
dr.metrics.sharedWithMeFetchDashboardsRequestsDuration.WithLabelValues("success").Observe(time.Since(start).Seconds())
|
|
}(time.Now())
|
|
}
|
|
|
|
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesCliDashboards) {
|
|
if query.OrgId == 0 {
|
|
requester, err := identity.GetRequester(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.OrgId = requester.GetOrgID()
|
|
}
|
|
|
|
response, err := dr.searchDashboardsThroughK8sRaw(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits))
|
|
for i, hit := range response.Hits {
|
|
finalResults[i] = dashboards.DashboardSearchProjection{
|
|
UID: hit.Name,
|
|
OrgID: query.OrgId,
|
|
Title: hit.Title,
|
|
Slug: slugify.Slugify(hit.Title),
|
|
IsFolder: false,
|
|
FolderUID: hit.Folder,
|
|
Tags: hit.Tags,
|
|
}
|
|
}
|
|
|
|
return finalResults, nil
|
|
}
|
|
|
|
return dr.dashboardStore.FindDashboards(ctx, query)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards")
|
|
defer span.End()
|
|
|
|
res, err := dr.FindDashboards(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hits := makeQueryResult(query, res)
|
|
return hits, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetAllDashboards(ctx context.Context) ([]*dashboards.Dashboard, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
requester, err := identity.GetRequester(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dr.listDashboardsThroughK8s(ctx, requester.GetOrgID())
|
|
}
|
|
|
|
return dr.dashboardStore.GetAllDashboards(ctx)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) {
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
|
return dr.listDashboardsThroughK8s(ctx, orgID)
|
|
}
|
|
|
|
return dr.dashboardStore.GetAllDashboardsByOrgId(ctx, orgID)
|
|
}
|
|
|
|
func getHitType(item dashboards.DashboardSearchProjection) model.HitType {
|
|
var hitType model.HitType
|
|
if item.IsFolder {
|
|
hitType = model.DashHitFolder
|
|
} else {
|
|
hitType = model.DashHitDB
|
|
}
|
|
|
|
return hitType
|
|
}
|
|
|
|
func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) model.HitList {
|
|
hitList := make([]*model.Hit, 0)
|
|
hits := make(map[string]*model.Hit)
|
|
|
|
for _, item := range res {
|
|
key := fmt.Sprintf("%s-%d", item.UID, item.OrgID)
|
|
hit, exists := hits[key]
|
|
if !exists {
|
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
|
hit = &model.Hit{
|
|
ID: item.ID,
|
|
UID: item.UID,
|
|
OrgID: item.OrgID,
|
|
Title: item.Title,
|
|
URI: "db/" + item.Slug,
|
|
URL: dashboards.GetDashboardFolderURL(item.IsFolder, item.UID, item.Slug),
|
|
Type: getHitType(item),
|
|
FolderID: item.FolderID, // nolint:staticcheck
|
|
FolderUID: item.FolderUID,
|
|
FolderTitle: item.FolderTitle,
|
|
Tags: []string{},
|
|
}
|
|
|
|
// when searching through unified storage, the dashboard will come as one
|
|
// item, when searching through legacy, the dashboard will come multiple times
|
|
// per tag. So we need to add the array here for unified, and the term below for legacy.
|
|
if item.Tags != nil {
|
|
hit.Tags = item.Tags
|
|
}
|
|
|
|
// nolint:staticcheck
|
|
if item.FolderID > 0 {
|
|
hit.FolderURL = dashboards.GetFolderURL(item.FolderUID, item.FolderSlug)
|
|
}
|
|
|
|
if query.Sort.MetaName != "" {
|
|
hit.SortMeta = item.SortMeta
|
|
hit.SortMetaName = query.Sort.MetaName
|
|
}
|
|
|
|
hitList = append(hitList, hit)
|
|
hits[key] = hit
|
|
}
|
|
if len(item.Term) > 0 {
|
|
hit.Tags = append(hit.Tags, item.Term)
|
|
}
|
|
if item.Deleted != nil {
|
|
deletedDate := (*item.Deleted).Add(daysInTrash)
|
|
hit.IsDeleted = true
|
|
hit.PermanentlyDeleteDate = &deletedDate
|
|
}
|
|
}
|
|
return hitList
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) {
|
|
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesCliDashboards) {
|
|
res, err := dr.k8sclient.getSearcher().Search(ctx, &resource.ResourceSearchRequest{
|
|
Options: &resource.ListOptions{
|
|
Key: &resource.ResourceKey{
|
|
Namespace: dr.k8sclient.getNamespace(query.OrgID),
|
|
Group: "dashboard.grafana.app",
|
|
Resource: "dashboards",
|
|
},
|
|
},
|
|
Facet: map[string]*resource.ResourceSearchRequest_Facet{
|
|
"tags": {
|
|
Field: "tags",
|
|
Limit: 100000,
|
|
},
|
|
},
|
|
Limit: 100000})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
facet, ok := res.Facet["tags"]
|
|
if !ok {
|
|
return []*dashboards.DashboardTagCloudItem{}, nil
|
|
}
|
|
|
|
results := make([]*dashboards.DashboardTagCloudItem, len(facet.Terms))
|
|
for i, item := range facet.Terms {
|
|
results[i] = &dashboards.DashboardTagCloudItem{
|
|
Term: item.Term,
|
|
Count: int(item.Count),
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
return dr.dashboardStore.GetDashboardTags(ctx, query)
|
|
}
|
|
|
|
func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) {
|
|
return dr.dashboardStore.CountDashboardsInFolders(ctx, &dashboards.CountDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID})
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) error {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.DeleteInFolders")
|
|
defer span.End()
|
|
|
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
|
return dr.dashboardStore.SoftDeleteDashboardsInFolders(ctx, orgID, folderUIDs)
|
|
}
|
|
|
|
return dr.dashboardStore.DeleteDashboardsInFolders(ctx, &dashboards.DeleteDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID})
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard }
|
|
|
|
func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (int64, error) {
|
|
ctx, span := tracer.Start(ctx, "dashboards.service.CleanUpDeletedDashboards")
|
|
defer span.End()
|
|
|
|
var deletedDashboardsCount int64
|
|
deletedDashboards, err := dr.dashboardStore.GetSoftDeletedExpiredDashboards(ctx, daysInTrash)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for _, dashboard := range deletedDashboards {
|
|
err = dr.DeleteDashboard(ctx, dashboard.ID, dashboard.UID, dashboard.OrgID)
|
|
if err != nil {
|
|
dr.log.Warn("Failed to cleanup deleted dashboard", "dashboardUid", dashboard.UID, "error", err)
|
|
break
|
|
}
|
|
deletedDashboardsCount++
|
|
}
|
|
|
|
return deletedDashboardsCount, nil
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------
|
|
// Dashboard k8s functions
|
|
// -----------------------------------------------------------------------------------------
|
|
|
|
func (dk8s *dashk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
|
|
cfg := dk8s.restConfigProvider.GetRestConfig(ctx)
|
|
if cfg == nil {
|
|
return nil, false
|
|
}
|
|
|
|
dyn, err := dynamic.NewForConfig(cfg)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
|
|
return dyn.Resource(dk8s.gvr).Namespace(dk8s.getNamespace(orgID)), true
|
|
}
|
|
|
|
func (dk8s *dashk8sHandler) getNamespace(orgID int64) string {
|
|
return dk8s.namespacer(orgID)
|
|
}
|
|
|
|
func (dk8s *dashk8sHandler) getSearcher() resource.ResourceIndexClient {
|
|
return dk8s.searcher
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) getK8sContext(ctx context.Context) (context.Context, context.CancelFunc, error) {
|
|
requester, requesterErr := identity.GetRequester(ctx)
|
|
if requesterErr != nil {
|
|
return nil, nil, requesterErr
|
|
}
|
|
|
|
user, exists := k8sRequest.UserFrom(ctx)
|
|
if !exists {
|
|
// add in k8s user if not there yet
|
|
var ok bool
|
|
user, ok = requester.(k8sUser.Info)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("could not convert user to k8s user")
|
|
}
|
|
}
|
|
|
|
newCtx := k8sRequest.WithUser(context.Background(), user)
|
|
newCtx = log.WithContextualAttributes(newCtx, log.FromContext(ctx))
|
|
// TODO: after GLSA token workflow is removed, make this return early
|
|
// and move the else below to be unconditional
|
|
if requesterErr == nil {
|
|
newCtxWithRequester := identity.WithRequester(newCtx, requester)
|
|
newCtx = newCtxWithRequester
|
|
}
|
|
|
|
// inherit the deadline from the original context, if it exists
|
|
deadline, ok := ctx.Deadline()
|
|
if ok {
|
|
var newCancel context.CancelFunc
|
|
newCtx, newCancel = context.WithTimeout(newCtx, time.Until(deadline))
|
|
return newCtx, newCancel, nil
|
|
}
|
|
|
|
return newCtx, nil, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
|
|
// create a new context - prevents issues when the request stems from the k8s api itself
|
|
// otherwise the context goes through the handlers twice and causes issues
|
|
newCtx, cancel, err := dr.getK8sContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
|
|
client, ok := dr.k8sclient.getClient(newCtx, query.OrgID)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
// if including deleted dashboards for restore, use the /latest subresource
|
|
subresource := ""
|
|
if query.IncludeDeleted && dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesRestore) {
|
|
subresource = "latest"
|
|
}
|
|
|
|
// get uid if not passed in
|
|
if query.UID == "" {
|
|
result, err := dr.GetDashboardUIDByID(ctx, &dashboards.GetDashboardRefByIDQuery{
|
|
ID: query.ID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query.UID = result.UID
|
|
}
|
|
|
|
out, err := client.Get(newCtx, query.UID, v1.GetOptions{}, subresource)
|
|
if err != nil && !apierrors.IsNotFound(err) {
|
|
return nil, err
|
|
} else if err != nil || out == nil {
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
return dr.UnstructuredToLegacyDashboard(ctx, out, query.OrgID)
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.Context, cmd *dashboards.SaveDashboardCommand, provisioning *dashboards.DashboardProvisioning, unprovision bool) (*dashboards.Dashboard, error) {
|
|
// default to 1 if not set
|
|
if cmd.OrgID == 0 {
|
|
cmd.OrgID = 1
|
|
}
|
|
|
|
// create a new context - prevents issues when the request stems from the k8s api itself
|
|
// otherwise the context goes through the handlers twice and causes issues
|
|
newCtx, cancel, err := dr.getK8sContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
|
|
client, ok := dr.k8sclient.getClient(newCtx, cmd.OrgID)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
obj, err := LegacySaveCommandToUnstructured(cmd, dr.k8sclient.getNamespace(cmd.OrgID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
annotations := obj.GetAnnotations()
|
|
if annotations == nil {
|
|
annotations = map[string]string{}
|
|
}
|
|
if unprovision {
|
|
delete(annotations, utils.AnnoKeyRepoName)
|
|
delete(annotations, utils.AnnoKeyRepoPath)
|
|
delete(annotations, utils.AnnoKeyRepoHash)
|
|
delete(annotations, utils.AnnoKeyRepoTimestamp)
|
|
} else {
|
|
annotations[utils.AnnoKeyRepoName] = provisionedFileNameWithPrefix(provisioning.Name)
|
|
annotations[utils.AnnoKeyRepoPath] = provisioning.ExternalID
|
|
annotations[utils.AnnoKeyRepoHash] = provisioning.CheckSum
|
|
annotations[utils.AnnoKeyRepoTimestamp] = time.Unix(provisioning.Updated, 0).UTC().Format(time.RFC3339)
|
|
}
|
|
obj.SetAnnotations(annotations)
|
|
|
|
var out *unstructured.Unstructured
|
|
current, err := client.Get(newCtx, obj.GetName(), v1.GetOptions{})
|
|
if err != nil && !apierrors.IsNotFound(err) {
|
|
return nil, err
|
|
} else if current == nil || (err != nil && apierrors.IsNotFound(err)) {
|
|
out, err = client.Create(newCtx, &obj, v1.CreateOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
out, err = client.Update(newCtx, &obj, v1.UpdateOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
finalDash, err := dr.UnstructuredToLegacyDashboard(ctx, out, cmd.OrgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return finalDash, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd *dashboards.SaveDashboardCommand, orgID int64) (*dashboards.Dashboard, error) {
|
|
// create a new context - prevents issues when the request stems from the k8s api itself
|
|
// otherwise the context goes through the handlers twice and causes issues
|
|
newCtx, cancel, err := dr.getK8sContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
|
|
client, ok := dr.k8sclient.getClient(newCtx, orgID)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
obj, err := LegacySaveCommandToUnstructured(cmd, dr.k8sclient.getNamespace(orgID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
setPluginID(obj, cmd.PluginID)
|
|
|
|
var out *unstructured.Unstructured
|
|
current, err := client.Get(newCtx, obj.GetName(), v1.GetOptions{})
|
|
if current == nil || err != nil {
|
|
out, err = client.Create(newCtx, &obj, v1.CreateOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
out, err = client.Update(newCtx, &obj, v1.UpdateOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
finalDash, err := dr.UnstructuredToLegacyDashboard(ctx, out, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return finalDash, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) deleteAllDashboardThroughK8s(ctx context.Context, orgID int64) error {
|
|
// create a new context - prevents issues when the request stems from the k8s api itself
|
|
// otherwise the context goes through the handlers twice and causes issues
|
|
newCtx, cancel, err := dr.getK8sContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
} else if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
|
|
client, ok := dr.k8sclient.getClient(newCtx, orgID)
|
|
if !ok {
|
|
return fmt.Errorf("could not get k8s client")
|
|
}
|
|
|
|
err = client.DeleteCollection(newCtx, v1.DeleteOptions{}, v1.ListOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, cmd *dashboards.DeleteDashboardCommand, validateProvisionedDashboard bool) error {
|
|
// create a new context - prevents issues when the request stems from the k8s api itself
|
|
// otherwise the context goes through the handlers twice and causes issues
|
|
newCtx, cancel, err := dr.getK8sContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
} else if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
|
|
client, ok := dr.k8sclient.getClient(newCtx, cmd.OrgID)
|
|
if !ok {
|
|
return fmt.Errorf("could not get k8s client")
|
|
}
|
|
|
|
// get uid if not passed in
|
|
if cmd.UID == "" {
|
|
result, err := dr.GetDashboardUIDByID(ctx, &dashboards.GetDashboardRefByIDQuery{
|
|
ID: cmd.ID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.UID = result.UID
|
|
}
|
|
|
|
// use a grace period of 0 to indicate to skip the check of deleting provisioned dashboards
|
|
var gracePeriod *int64
|
|
if !validateProvisionedDashboard {
|
|
noGracePeriod := int64(0)
|
|
gracePeriod = &noGracePeriod
|
|
}
|
|
|
|
err = client.Delete(newCtx, cmd.UID, v1.DeleteOptions{
|
|
GracePeriodSeconds: gracePeriod,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) listDashboardsThroughK8s(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) {
|
|
// create a new context - prevents issues when the request stems from the k8s api itself
|
|
// otherwise the context goes through the handlers twice and causes issues
|
|
newCtx, cancel, err := dr.getK8sContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
|
|
client, ok := dr.k8sclient.getClient(newCtx, orgID)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
out, err := client.List(newCtx, v1.ListOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
} else if out == nil {
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
dashboards := make([]*dashboards.Dashboard, 0)
|
|
for _, item := range out.Items {
|
|
dash, err := dr.UnstructuredToLegacyDashboard(ctx, &item, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dashboards = append(dashboards, dash)
|
|
}
|
|
|
|
return dashboards, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (*v0alpha1.SearchResults, error) {
|
|
dashboardskey := &resource.ResourceKey{
|
|
Namespace: dr.k8sclient.getNamespace(query.OrgId),
|
|
Group: "dashboard.grafana.app",
|
|
Resource: "dashboards",
|
|
}
|
|
|
|
request := &resource.ResourceSearchRequest{
|
|
Options: &resource.ListOptions{
|
|
Key: dashboardskey,
|
|
Fields: []*resource.Requirement{},
|
|
Labels: []*resource.Requirement{},
|
|
},
|
|
Limit: 100000}
|
|
|
|
if len(query.DashboardUIDs) > 0 {
|
|
request.Options.Fields = []*resource.Requirement{{
|
|
Key: "name",
|
|
Operator: string(selection.In),
|
|
Values: query.DashboardUIDs,
|
|
}}
|
|
} else if len(query.DashboardIds) > 0 {
|
|
values := make([]string, len(query.DashboardIds))
|
|
for i, id := range query.DashboardIds {
|
|
values[i] = strconv.FormatInt(id, 10)
|
|
}
|
|
|
|
request.Options.Labels = append(request.Options.Labels, &resource.Requirement{
|
|
Key: utils.LabelKeyDeprecatedInternalID, // nolint:staticcheck
|
|
Operator: string(selection.In),
|
|
Values: values,
|
|
})
|
|
}
|
|
|
|
if len(query.FolderUIDs) > 0 {
|
|
req := []*resource.Requirement{{
|
|
Key: "folder",
|
|
Operator: string(selection.In),
|
|
Values: query.FolderUIDs,
|
|
}}
|
|
request.Options.Fields = append(request.Options.Fields, req...)
|
|
}
|
|
|
|
if query.ProvisionedRepo != "" {
|
|
req := []*resource.Requirement{{
|
|
Key: "repo.name",
|
|
Operator: string(selection.In),
|
|
Values: []string{query.ProvisionedRepo},
|
|
}}
|
|
request.Options.Fields = append(request.Options.Fields, req...)
|
|
}
|
|
|
|
if len(query.ProvisionedReposNotIn) > 0 {
|
|
req := []*resource.Requirement{{
|
|
Key: "repo.name",
|
|
Operator: string(selection.NotIn),
|
|
Values: query.ProvisionedReposNotIn,
|
|
}}
|
|
request.Options.Fields = append(request.Options.Fields, req...)
|
|
}
|
|
if query.ProvisionedPath != "" {
|
|
req := []*resource.Requirement{{
|
|
Key: "repo.path",
|
|
Operator: string(selection.In),
|
|
Values: []string{query.ProvisionedPath},
|
|
}}
|
|
request.Options.Fields = append(request.Options.Fields, req...)
|
|
}
|
|
|
|
// note: this does not allow for partial matching
|
|
//
|
|
// partial matching will be allowed through the api layer for the frontend,
|
|
// but is currently not needed by other services in the backend
|
|
if query.Title != "" {
|
|
req := []*resource.Requirement{{
|
|
Key: "title",
|
|
Operator: string(selection.In),
|
|
Values: []string{query.Title},
|
|
}}
|
|
request.Options.Fields = append(request.Options.Fields, req...)
|
|
}
|
|
|
|
if len(query.Tags) > 0 {
|
|
req := []*resource.Requirement{{
|
|
Key: "tags",
|
|
Operator: string(selection.In),
|
|
Values: query.Tags,
|
|
}}
|
|
request.Options.Fields = append(request.Options.Fields, req...)
|
|
}
|
|
|
|
if query.Limit > 0 {
|
|
request.Limit = query.Limit
|
|
}
|
|
|
|
res, err := dr.k8sclient.getSearcher().Search(ctx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ParseResults(res, 0)
|
|
}
|
|
|
|
type dashboardProvisioningWithUID struct {
|
|
dashboards.DashboardProvisioning
|
|
DashboardUID string
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx context.Context, query dashboards.FindPersistedDashboardsQuery) ([]*dashboardProvisioningWithUID, error) {
|
|
ctx = identity.WithRequester(ctx, getDashboardBackgroundRequester(query.OrgId))
|
|
|
|
if query.ProvisionedRepo != "" {
|
|
query.ProvisionedRepo = provisionedFileNameWithPrefix(query.ProvisionedRepo)
|
|
}
|
|
|
|
if len(query.ProvisionedReposNotIn) > 0 {
|
|
repos := make([]string, len(query.ProvisionedReposNotIn))
|
|
for i, v := range query.ProvisionedReposNotIn {
|
|
repos[i] = provisionedFileNameWithPrefix(v)
|
|
}
|
|
query.ProvisionedReposNotIn = repos
|
|
}
|
|
|
|
searchResults, err := dr.searchDashboardsThroughK8sRaw(ctx, &query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newCtx, cancel, err := dr.getK8sContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if cancel != nil {
|
|
defer cancel()
|
|
}
|
|
|
|
client, ok := dr.k8sclient.getClient(newCtx, query.OrgId)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
// loop through all hits concurrently to get the repo information (if set due to file provisioning)
|
|
dashs := make([]*dashboardProvisioningWithUID, 0)
|
|
var mu sync.Mutex
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
for _, h := range searchResults.Hits {
|
|
func(hit v0alpha1.DashboardHit) {
|
|
g.Go(func() error {
|
|
out, err := client.Get(ctx, hit.Name, v1.GetOptions{}, "")
|
|
if err != nil {
|
|
return err
|
|
} else if out == nil {
|
|
return dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
meta, err := utils.MetaAccessor(out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// ensure the repo is set due to file provisioning, otherwise skip it
|
|
fileRepo, found := getProvisionedFileNameFromMeta(meta)
|
|
if !found {
|
|
return nil
|
|
}
|
|
|
|
provisioning := &dashboardProvisioningWithUID{
|
|
DashboardUID: hit.Name,
|
|
}
|
|
provisioning.Name = fileRepo
|
|
provisioning.ExternalID = meta.GetRepositoryPath()
|
|
provisioning.CheckSum = meta.GetRepositoryHash()
|
|
provisioning.DashboardID = meta.GetDeprecatedInternalID() // nolint:staticcheck
|
|
|
|
updated, err := meta.GetRepositoryTimestamp()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if updated != nil {
|
|
provisioning.Updated = updated.Unix()
|
|
}
|
|
|
|
mu.Lock()
|
|
dashs = append(dashs, provisioning)
|
|
mu.Unlock()
|
|
|
|
return nil
|
|
})
|
|
}(h)
|
|
}
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return dashs, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboards.Dashboard, error) {
|
|
response, err := dr.searchDashboardsThroughK8sRaw(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]*dashboards.Dashboard, len(response.Hits))
|
|
for i, hit := range response.Hits {
|
|
result[i] = &dashboards.Dashboard{
|
|
OrgID: query.OrgId,
|
|
UID: hit.Name,
|
|
Slug: slugify.Slugify(hit.Title),
|
|
Title: hit.Title,
|
|
FolderUID: hit.Folder,
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alpha1.SearchResults, error) {
|
|
if result == nil {
|
|
return nil, nil
|
|
} else if result.Error != nil {
|
|
return nil, fmt.Errorf("%d error searching: %s: %s", result.Error.Code, result.Error.Message, result.Error.Details)
|
|
} else if result.Results == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
titleIDX := 0
|
|
folderIDX := 1
|
|
tagsIDX := -1
|
|
scoreIDX := 0
|
|
explainIDX := 0
|
|
|
|
for i, v := range result.Results.Columns {
|
|
switch v.Name {
|
|
case resource.SEARCH_FIELD_EXPLAIN:
|
|
explainIDX = i
|
|
case resource.SEARCH_FIELD_SCORE:
|
|
scoreIDX = i
|
|
case "title":
|
|
titleIDX = i
|
|
case "folder":
|
|
folderIDX = i
|
|
case "tags":
|
|
tagsIDX = i
|
|
}
|
|
}
|
|
|
|
sr := &v0alpha1.SearchResults{
|
|
Offset: offset,
|
|
TotalHits: result.TotalHits,
|
|
QueryCost: result.QueryCost,
|
|
MaxScore: result.MaxScore,
|
|
Hits: make([]v0alpha1.DashboardHit, len(result.Results.Rows)),
|
|
}
|
|
|
|
for i, row := range result.Results.Rows {
|
|
hit := &v0alpha1.DashboardHit{
|
|
Resource: row.Key.Resource, // folders | dashboards
|
|
Name: row.Key.Name, // The Grafana UID
|
|
Title: string(row.Cells[titleIDX]),
|
|
Folder: string(row.Cells[folderIDX]),
|
|
}
|
|
if tagsIDX > 0 && row.Cells[tagsIDX] != nil {
|
|
_ = json.Unmarshal(row.Cells[tagsIDX], &hit.Tags)
|
|
}
|
|
if explainIDX > 0 && row.Cells[explainIDX] != nil {
|
|
_ = json.Unmarshal(row.Cells[explainIDX], &hit.Explain)
|
|
}
|
|
if scoreIDX > 0 && row.Cells[scoreIDX] != nil {
|
|
_, _ = binary.Decode(row.Cells[scoreIDX], binary.BigEndian, &hit.Score)
|
|
}
|
|
|
|
sr.Hits[i] = *hit
|
|
}
|
|
|
|
// Add facet results
|
|
if result.Facet != nil {
|
|
sr.Facets = make(map[string]v0alpha1.FacetResult)
|
|
for k, v := range result.Facet {
|
|
sr.Facets[k] = v0alpha1.FacetResult{
|
|
Field: v.Field,
|
|
Total: v.Total,
|
|
Missing: v.Missing,
|
|
Terms: make([]v0alpha1.TermFacet, len(v.Terms)),
|
|
}
|
|
for j, t := range v.Terms {
|
|
sr.Facets[k].Terms[j] = v0alpha1.TermFacet{
|
|
Term: t.Term,
|
|
Count: t.Count,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return sr, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) {
|
|
spec, ok := item.Object["spec"].(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("error parsing dashboard from k8s response")
|
|
}
|
|
obj, err := utils.MetaAccessor(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uid := obj.GetName()
|
|
spec["uid"] = uid
|
|
|
|
dashVersion := 0
|
|
if version, ok := spec["version"].(int64); ok {
|
|
dashVersion = int(version)
|
|
}
|
|
|
|
out := dashboards.Dashboard{
|
|
OrgID: orgID,
|
|
ID: obj.GetDeprecatedInternalID(), // nolint:staticcheck
|
|
UID: uid,
|
|
Slug: obj.GetSlug(),
|
|
FolderUID: obj.GetFolder(),
|
|
Version: dashVersion,
|
|
Data: simplejson.NewFromAny(spec),
|
|
}
|
|
|
|
out.Created = obj.GetCreationTimestamp().Time
|
|
updated, err := obj.GetUpdatedTimestamp()
|
|
if err == nil && updated != nil {
|
|
out.Updated = *updated
|
|
} else {
|
|
// by default, set updated to created
|
|
out.Updated = out.Created
|
|
}
|
|
|
|
deleted := obj.GetDeletionTimestamp()
|
|
if deleted != nil {
|
|
out.Deleted = obj.GetDeletionTimestamp().Time
|
|
}
|
|
|
|
out.PluginID = GetPluginIDFromMeta(obj)
|
|
|
|
creator, err := dr.getUserFromMeta(ctx, obj.GetCreatedBy())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.CreatedBy = creator.ID
|
|
|
|
updater, err := dr.getUserFromMeta(ctx, obj.GetUpdatedBy())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.UpdatedBy = updater.ID
|
|
|
|
// any dashboards that have already been synced to unified storage will have the id in the spec
|
|
// and not as a label. We will need to support this conversion until they have all been updated
|
|
// to labels
|
|
if id, ok := spec["id"].(int64); ok {
|
|
out.ID = id
|
|
out.Data.Del("id")
|
|
}
|
|
|
|
if gnetID, ok := spec["gnet_id"].(int64); ok {
|
|
out.GnetID = gnetID
|
|
}
|
|
|
|
if isFolder, ok := spec["is_folder"].(bool); ok {
|
|
out.IsFolder = isFolder
|
|
}
|
|
|
|
if hasACL, ok := spec["has_acl"].(bool); ok {
|
|
out.HasACL = hasACL
|
|
}
|
|
|
|
if title, ok := spec["title"].(string); ok {
|
|
out.Title = title
|
|
// if slug isn't in the metadata, add it via the title
|
|
if out.Slug == "" {
|
|
out.UpdateSlug()
|
|
}
|
|
}
|
|
|
|
return &out, nil
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) getUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) {
|
|
if userMeta == "" || toUID(userMeta) == "" {
|
|
return &user.User{}, nil
|
|
}
|
|
usr, err := dr.getUser(ctx, toUID(userMeta))
|
|
if err != nil && errors.Is(err, user.ErrUserNotFound) {
|
|
return &user.User{}, nil
|
|
}
|
|
return usr, err
|
|
}
|
|
|
|
func (dr *DashboardServiceImpl) getUser(ctx context.Context, uid string) (*user.User, error) {
|
|
userId, err := strconv.ParseInt(uid, 10, 64)
|
|
if err == nil {
|
|
return dr.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userId})
|
|
}
|
|
return dr.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: uid})
|
|
}
|
|
|
|
var pluginIDRepoName = "plugin"
|
|
var fileProvisionedRepoPrefix = "file:"
|
|
|
|
func setPluginID(obj unstructured.Unstructured, pluginID string) {
|
|
if pluginID == "" {
|
|
return
|
|
}
|
|
|
|
annotations := obj.GetAnnotations()
|
|
if annotations == nil {
|
|
annotations = map[string]string{}
|
|
}
|
|
annotations[utils.AnnoKeyRepoName] = pluginIDRepoName
|
|
annotations[utils.AnnoKeyRepoPath] = pluginID
|
|
obj.SetAnnotations(annotations)
|
|
}
|
|
|
|
func provisionedFileNameWithPrefix(name string) string {
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
|
|
return fileProvisionedRepoPrefix + name
|
|
}
|
|
|
|
func getProvisionedFileNameFromMeta(obj utils.GrafanaMetaAccessor) (string, bool) {
|
|
return strings.CutPrefix(obj.GetRepositoryName(), fileProvisionedRepoPrefix)
|
|
}
|
|
|
|
func GetPluginIDFromMeta(obj utils.GrafanaMetaAccessor) string {
|
|
if obj.GetRepositoryName() == pluginIDRepoName {
|
|
return obj.GetRepositoryPath()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, namespace string) (unstructured.Unstructured, error) {
|
|
uid := cmd.GetDashboardModel().UID
|
|
if uid == "" {
|
|
uid = uuid.NewString()
|
|
}
|
|
|
|
finalObj := unstructured.Unstructured{
|
|
Object: map[string]interface{}{},
|
|
}
|
|
|
|
obj := map[string]interface{}{}
|
|
body, err := cmd.Dashboard.ToDB()
|
|
if err != nil {
|
|
return finalObj, err
|
|
}
|
|
|
|
err = json.Unmarshal(body, &obj)
|
|
if err != nil {
|
|
return finalObj, err
|
|
}
|
|
|
|
// update the version
|
|
version, ok := obj["version"].(float64)
|
|
if !ok || version == 0 {
|
|
obj["version"] = 1
|
|
} else if !cmd.Overwrite {
|
|
obj["version"] = version + 1
|
|
}
|
|
|
|
finalObj.Object["spec"] = obj
|
|
finalObj.SetName(uid)
|
|
finalObj.SetNamespace(namespace)
|
|
finalObj.SetGroupVersionKind(v0alpha1.DashboardResourceInfo.GroupVersionKind())
|
|
|
|
return finalObj, nil
|
|
}
|
|
|
|
func toUID(rawIdentifier string) string {
|
|
parts := strings.Split(rawIdentifier, ":")
|
|
if len(parts) < 2 {
|
|
return ""
|
|
}
|
|
return parts[1]
|
|
}
|
|
|