mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 00:36:23 +08:00
418 lines
12 KiB
Go
418 lines
12 KiB
Go
package dashverimpl
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/services/apiserver"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/search/sort"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
|
)
|
|
|
|
const (
|
|
maxVersionsToDeletePerBatch = 100
|
|
maxVersionDeletionBatches = 50
|
|
)
|
|
|
|
type Service struct {
|
|
cfg *setting.Cfg
|
|
store store
|
|
dashSvc dashboards.DashboardService
|
|
k8sclient client.K8sHandler
|
|
features featuremgmt.FeatureToggles
|
|
log log.Logger
|
|
}
|
|
|
|
func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, features featuremgmt.FeatureToggles,
|
|
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, dual dualwrite.Service, sorter sort.Service) dashver.Service {
|
|
return &Service{
|
|
cfg: cfg,
|
|
store: &sqlStore{
|
|
db: db,
|
|
dialect: db.GetDialect(),
|
|
},
|
|
features: features,
|
|
k8sclient: client.NewK8sHandler(
|
|
dual,
|
|
request.GetNamespaceMapper(cfg),
|
|
dashv0.DashboardResourceInfo.GroupVersionResource(),
|
|
restConfigProvider.GetRestConfig,
|
|
dashboardStore,
|
|
userService,
|
|
unified,
|
|
sorter,
|
|
),
|
|
dashSvc: dashboardService,
|
|
log: log.New("dashboard-version"),
|
|
}
|
|
}
|
|
|
|
func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQuery) (*dashver.DashboardVersionDTO, error) {
|
|
// Get the DashboardUID if not populated
|
|
if query.DashboardUID == "" {
|
|
u, err := s.getDashUIDMaybeEmpty(ctx, query.DashboardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.DashboardUID = u
|
|
}
|
|
|
|
// The store methods require the dashboard ID (uid is not in the dashboard
|
|
// versions table, at time of this writing), so get the DashboardID if it
|
|
// was not populated.
|
|
if query.DashboardID == 0 {
|
|
id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.DashboardID = id
|
|
}
|
|
|
|
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
|
version, err := s.getHistoryThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
version, err := s.store.Get(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
version.Data.Set("id", version.DashboardID)
|
|
return version.ToDTO(query.DashboardUID), nil
|
|
}
|
|
|
|
func (s *Service) DeleteExpired(ctx context.Context, cmd *dashver.DeleteExpiredVersionsCommand) error {
|
|
versionsToKeep := s.cfg.DashboardVersionsToKeep
|
|
if versionsToKeep < 1 {
|
|
versionsToKeep = 1
|
|
}
|
|
|
|
for batch := 0; batch < maxVersionDeletionBatches; batch++ {
|
|
versionIdsToDelete, batchErr := s.store.GetBatch(ctx, cmd, maxVersionsToDeletePerBatch, versionsToKeep)
|
|
if batchErr != nil {
|
|
return batchErr
|
|
}
|
|
|
|
if len(versionIdsToDelete) < 1 {
|
|
return nil
|
|
}
|
|
|
|
deleted, err := s.store.DeleteBatch(ctx, cmd, versionIdsToDelete)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.DeletedRows += deleted
|
|
|
|
if deleted < int64(maxVersionsToDeletePerBatch) {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List all dashboard versions for the given dashboard ID.
|
|
func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) (*dashver.DashboardVersionResponse, error) {
|
|
// Get the DashboardUID if not populated
|
|
if query.DashboardUID == "" {
|
|
u, err := s.getDashUIDMaybeEmpty(ctx, query.DashboardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.DashboardUID = u
|
|
}
|
|
|
|
// The store methods require the dashboard ID (uid is not in the dashboard
|
|
// versions table, at time of this writing), so get the DashboardID if it
|
|
// was not populated.
|
|
if query.DashboardID == 0 {
|
|
id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.DashboardID = id
|
|
}
|
|
if query.Limit == 0 {
|
|
query.Limit = 1000
|
|
}
|
|
|
|
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
|
versions, err := s.listHistoryThroughK8s(
|
|
ctx,
|
|
query.OrgID,
|
|
query.DashboardUID,
|
|
int64(query.Limit),
|
|
query.ContinueToken,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return versions, nil
|
|
}
|
|
|
|
dvs, err := s.store.List(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dtos := make([]*dashver.DashboardVersionDTO, len(dvs))
|
|
for i, v := range dvs {
|
|
dtos[i] = v.ToDTO(query.DashboardUID)
|
|
}
|
|
return &dashver.DashboardVersionResponse{
|
|
Versions: dtos,
|
|
}, nil
|
|
}
|
|
|
|
// getDashUIDMaybeEmpty is a helper function which takes a dashboardID and
|
|
// returns the UID. If the dashboard is not found, it will return an empty
|
|
// string.
|
|
func (s *Service) getDashUIDMaybeEmpty(ctx context.Context, id int64) (string, error) {
|
|
q := dashboards.GetDashboardRefByIDQuery{ID: id}
|
|
result, err := s.dashSvc.GetDashboardUIDByID(ctx, &q)
|
|
if err != nil {
|
|
if errors.Is(err, dashboards.ErrDashboardNotFound) {
|
|
s.log.Debug("dashboard not found")
|
|
return "", nil
|
|
} else {
|
|
s.log.Error("error getting dashboard", err)
|
|
return "", err
|
|
}
|
|
}
|
|
return result.UID, nil
|
|
}
|
|
|
|
// getDashIDMaybeEmpty is a helper function which takes a dashboardUID and
|
|
// returns the ID. If the dashboard is not found, it will return -1.
|
|
func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string, orgID int64) (int64, error) {
|
|
q := dashboards.GetDashboardQuery{UID: uid, OrgID: orgID}
|
|
result, err := s.dashSvc.GetDashboard(ctx, &q)
|
|
if err != nil {
|
|
if errors.Is(err, dashboards.ErrDashboardNotFound) {
|
|
s.log.Debug("dashboard not found")
|
|
return -1, nil
|
|
} else {
|
|
s.log.Error("error getting dashboard", err)
|
|
return -1, err
|
|
}
|
|
}
|
|
return result.ID, nil
|
|
}
|
|
|
|
func (s *Service) getHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, version int64) (*dashver.DashboardVersionDTO, error) {
|
|
// this is an unideal implementation - we have to list all versions and filter here, since there currently is no way to query for the
|
|
// generation id in unified storage, so we cannot query for the dashboard version directly, and we cannot use search as history is not indexed.
|
|
// use batches to make sure we don't load too much data at once.
|
|
const batchSize = 50
|
|
labelSelector := utils.LabelKeyGetHistory + "=true"
|
|
fieldSelector := "metadata.name=" + dashboardUID
|
|
var continueToken string
|
|
for {
|
|
out, err := s.k8sclient.List(ctx, orgID, v1.ListOptions{
|
|
LabelSelector: labelSelector,
|
|
FieldSelector: fieldSelector,
|
|
Limit: int64(batchSize),
|
|
Continue: continueToken,
|
|
})
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
if out == nil {
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
for _, item := range out.Items {
|
|
if item.GetGeneration() == version {
|
|
return s.UnstructuredToLegacyDashboardVersion(ctx, &item, orgID)
|
|
}
|
|
}
|
|
|
|
continueToken = out.GetContinue()
|
|
if continueToken == "" || len(out.Items) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
func (s *Service) listHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, limit int64, continueToken string) (*dashver.DashboardVersionResponse, error) {
|
|
labelSelector := utils.LabelKeyGetHistory + "=true"
|
|
fieldSelector := "metadata.name=" + dashboardUID
|
|
out, err := s.k8sclient.List(ctx, orgID, v1.ListOptions{
|
|
LabelSelector: labelSelector,
|
|
FieldSelector: fieldSelector,
|
|
Limit: limit,
|
|
Continue: continueToken,
|
|
})
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
if out == nil {
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
dashboards, err := s.UnstructuredToLegacyDashboardVersionList(ctx, out.Items, orgID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dashver.DashboardVersionResponse{
|
|
ContinueToken: out.GetContinue(),
|
|
Versions: dashboards,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashver.DashboardVersionDTO, error) {
|
|
obj, err := utils.MetaAccessor(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
users, err := s.k8sclient.GetUsersFromMeta(ctx, []string{obj.GetCreatedBy(), obj.GetUpdatedBy()})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.unstructuredToLegacyDashboardVersionWithUsers(item, users)
|
|
}
|
|
|
|
func (s *Service) UnstructuredToLegacyDashboardVersionList(ctx context.Context, items []unstructured.Unstructured, orgID int64) ([]*dashver.DashboardVersionDTO, error) {
|
|
// get users ahead of time to do just one db call, rather than 2 per item in the list
|
|
userMeta := []string{}
|
|
for _, item := range items {
|
|
obj, err := utils.MetaAccessor(&item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if obj.GetCreatedBy() != "" {
|
|
userMeta = append(userMeta, obj.GetCreatedBy())
|
|
}
|
|
if obj.GetUpdatedBy() != "" {
|
|
userMeta = append(userMeta, obj.GetUpdatedBy())
|
|
}
|
|
}
|
|
|
|
users, err := s.k8sclient.GetUsersFromMeta(ctx, userMeta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
versions := make([]*dashver.DashboardVersionDTO, len(items))
|
|
for i, item := range items {
|
|
version, err := s.unstructuredToLegacyDashboardVersionWithUsers(&item, users)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
versions[i] = version
|
|
}
|
|
|
|
return versions, nil
|
|
}
|
|
|
|
func (s *Service) unstructuredToLegacyDashboardVersionWithUsers(item *unstructured.Unstructured, users map[string]*user.User) (*dashver.DashboardVersionDTO, 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 := obj.GetGeneration()
|
|
parentVersion := dashVersion - 1
|
|
if parentVersion < 0 {
|
|
parentVersion = 0
|
|
}
|
|
if dashVersion > 0 {
|
|
spec["version"] = dashVersion
|
|
}
|
|
|
|
var createdBy *user.User
|
|
if creator, ok := users[obj.GetCreatedBy()]; ok {
|
|
createdBy = creator
|
|
}
|
|
// if updated by is set, then this version of the dashboard was "created"
|
|
// by that user
|
|
if updater, ok := users[obj.GetUpdatedBy()]; ok {
|
|
createdBy = updater
|
|
}
|
|
|
|
createdByID := int64(0)
|
|
if createdBy != nil {
|
|
createdByID = createdBy.ID
|
|
}
|
|
|
|
created := obj.GetCreationTimestamp().Time
|
|
if updated, err := obj.GetUpdatedTimestamp(); err == nil && updated != nil {
|
|
created = *updated
|
|
}
|
|
|
|
restoreVer, err := getRestoreVersion(obj.GetMessage())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dashver.DashboardVersionDTO{
|
|
ID: dashVersion,
|
|
DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck
|
|
DashboardUID: uid,
|
|
Created: created,
|
|
CreatedBy: createdByID,
|
|
Message: obj.GetMessage(),
|
|
RestoredFrom: restoreVer,
|
|
Version: int(dashVersion),
|
|
ParentVersion: int(parentVersion),
|
|
Data: simplejson.NewFromAny(spec),
|
|
}, nil
|
|
}
|
|
|
|
var restoreMsg = "Restored from version "
|
|
|
|
func DashboardRestoreMessage(version int) string {
|
|
return fmt.Sprintf("%s%d", restoreMsg, version)
|
|
}
|
|
|
|
func getRestoreVersion(msg string) (int, error) {
|
|
parts := strings.Split(msg, restoreMsg)
|
|
if len(parts) < 2 {
|
|
return 0, nil
|
|
}
|
|
|
|
ver, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return ver, nil
|
|
}
|