What This commit refactors the logic to restore a dashboard from a version. The logic is moved from the API handler to the dashboard versions service, which now supports restoring dashboards of different API versions. Why To make sure that dashboard version restoration works with v2 dashboards API, as well as future API versions. Signed-off-by: Igor Suleymanov <igor.suleymanov@grafana.com>
693 lines
20 KiB
Go
693 lines
20 KiB
Go
package dashverimpl
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
"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"
|
|
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
|
|
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"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/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
dashboardclient "github.com/grafana/grafana/pkg/services/dashboards/service/client"
|
|
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl")
|
|
|
|
const (
|
|
maxVersionsToDeletePerBatch = 100
|
|
maxVersionDeletionBatches = 50
|
|
)
|
|
|
|
type Service struct {
|
|
cfg *setting.Cfg
|
|
store store
|
|
dashSvc dashboards.DashboardService
|
|
k8sclient dashboardclient.K8sHandlerWithFallback
|
|
features featuremgmt.FeatureToggles
|
|
log log.Logger
|
|
}
|
|
|
|
func ProvideService(
|
|
cfg *setting.Cfg,
|
|
db db.DB,
|
|
dashboardService dashboards.DashboardService,
|
|
features featuremgmt.FeatureToggles,
|
|
clientWithFallback dashboardclient.K8sHandlerWithFallback,
|
|
) dashver.Service {
|
|
return &Service{
|
|
cfg: cfg,
|
|
store: &sqlStore{
|
|
db: db,
|
|
dialect: db.GetDialect(),
|
|
},
|
|
features: features,
|
|
k8sclient: clientWithFallback,
|
|
dashSvc: dashboardService,
|
|
log: log.New("dashboard-version"),
|
|
}
|
|
}
|
|
|
|
func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQuery) (*dashver.DashboardVersionDTO, error) {
|
|
if query.DashboardUID == "" {
|
|
u, err := s.getDashUIDMaybeEmpty(ctx, query.DashboardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.DashboardUID = u
|
|
}
|
|
|
|
versionObj, err := s.getDashboardVersionThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.transformUnstructuredToLegacyDTO(ctx, versionObj)
|
|
}
|
|
|
|
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) {
|
|
if query.DashboardUID == "" {
|
|
u, err := s.getDashUIDMaybeEmpty(ctx, query.DashboardID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.DashboardUID = u
|
|
}
|
|
|
|
if query.Limit == 0 {
|
|
query.Limit = 1000
|
|
}
|
|
|
|
list, err := s.listDashboardVersionsThroughK8s(
|
|
ctx,
|
|
query.OrgID,
|
|
query.DashboardUID,
|
|
int64(query.Limit),
|
|
query.ContinueToken,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dashboards, err := s.transformUnstructuredToLegacyDTOList(ctx, list.Items)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &dashver.DashboardVersionResponse{
|
|
ContinueToken: list.GetContinue(),
|
|
Versions: dashboards,
|
|
}, nil
|
|
}
|
|
|
|
// RestoreVersion restores a dashboard version.
|
|
func (s *Service) RestoreVersion(ctx context.Context, cmd *dashver.RestoreVersionCommand) (*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "Service.RestoreVersion")
|
|
defer span.End()
|
|
|
|
// Get dashboard UID if not provided
|
|
if cmd.DashboardUID == "" {
|
|
u, err := s.getDashUIDMaybeEmpty(ctx, cmd.DashboardID)
|
|
if err != nil {
|
|
s.log.Debug("error getting dashboard UID", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
cmd.DashboardUID = u
|
|
}
|
|
|
|
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) ||
|
|
s.features.IsEnabledGlobally(featuremgmt.FlagDashboardNewLayouts) {
|
|
s.log.Debug("restoring dashboard version through k8s")
|
|
res, err := s.restoreVersionThroughK8s(ctx, cmd)
|
|
if err != nil {
|
|
s.log.Debug("error restoring dashboard version through k8s", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
s.log.Debug("restoring dashboard version through legacy")
|
|
res, err := s.restoreVersionLegacy(ctx, cmd)
|
|
if err != nil {
|
|
s.log.Debug("error restoring dashboard version through legacy", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Service) getDashboardVersionThroughK8s(
|
|
ctx context.Context, orgID int64, dashboardUID string, version int64,
|
|
) (*unstructured.Unstructured, 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 &item, nil
|
|
}
|
|
}
|
|
|
|
continueToken = out.GetContinue()
|
|
if continueToken == "" || len(out.Items) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil, dashboards.ErrDashboardNotFound
|
|
}
|
|
|
|
func (s *Service) listDashboardVersionsThroughK8s(
|
|
ctx context.Context, orgID int64, dashboardUID string, limit int64, continueToken string,
|
|
) (*unstructured.UnstructuredList, 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
|
|
}
|
|
|
|
// if k8s returns a continue token, we need to fetch the next page(s) until we either reach the limit or there are no more pages
|
|
continueToken = out.GetContinue()
|
|
for (len(out.Items) < int(limit)) && (continueToken != "") {
|
|
tempOut, err := s.k8sclient.List(ctx, orgID, v1.ListOptions{
|
|
LabelSelector: labelSelector,
|
|
FieldSelector: fieldSelector,
|
|
Continue: continueToken,
|
|
Limit: limit - int64(len(out.Items)),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.Items = append(out.Items, tempOut.Items...)
|
|
continueToken = tempOut.GetContinue()
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Service) restoreVersionThroughK8s(
|
|
ctx context.Context, cmd *dashver.RestoreVersionCommand,
|
|
) (*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "Service.restoreVersionThroughK8s")
|
|
defer span.End()
|
|
|
|
// We must use separate gctx context here, because it will be canceled, once the group is done.
|
|
// If we use the same ctx after the call to g.Wait, it will already be canceled at that point,
|
|
// causing all subsequent context-using operations to immediately return with "context canceled" error.
|
|
g, gctx := errgroup.WithContext(ctx)
|
|
|
|
var current *unstructured.Unstructured
|
|
g.Go(func() error {
|
|
var err error
|
|
current, err = s.k8sclient.Get(gctx, cmd.DashboardUID, cmd.Requester.GetOrgID(), v1.GetOptions{})
|
|
if err != nil {
|
|
s.log.Debug("error getting current dashboard", "error", err)
|
|
}
|
|
return err
|
|
})
|
|
|
|
var version *unstructured.Unstructured
|
|
g.Go(func() error {
|
|
var err error
|
|
version, err = s.getDashboardVersionThroughK8s(gctx, cmd.Requester.GetOrgID(), cmd.DashboardUID, cmd.Version)
|
|
if err != nil {
|
|
s.log.Debug("error getting version", "error", err)
|
|
}
|
|
return err
|
|
})
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
// Compare dashboard data using the new version-aware comparator
|
|
identical, err := compareUnstructuredDashboards(version, current)
|
|
if err != nil {
|
|
s.log.Debug("error comparing dashboard versions", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
if identical {
|
|
return nil, dashboards.ErrDashboardRestoreIdenticalVersion
|
|
}
|
|
|
|
versionMeta, err := utils.MetaAccessor(version)
|
|
if err != nil {
|
|
s.log.Debug("error getting old version meta accessor", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
spec, err := versionMeta.GetSpec()
|
|
if err != nil {
|
|
s.log.Debug("error getting old version spec", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
currentMeta, err := utils.MetaAccessor(current)
|
|
if err != nil {
|
|
s.log.Debug("error getting current meta accessor", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
currentMeta.SetMessage(dashboardRestoreMessage(int(versionMeta.GetGeneration())))
|
|
if err := currentMeta.SetSpec(spec); err != nil {
|
|
s.log.Debug("error setting current version spec", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
updatedObj, err := s.k8sclient.Update(ctx, current, cmd.Requester.GetOrgID(), v1.UpdateOptions{})
|
|
if err != nil {
|
|
s.log.Debug("error updating dashboard to specified version", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
res, err := s.dashSvc.UnstructuredToLegacyDashboard(ctx, updatedObj, cmd.Requester.GetOrgID())
|
|
if err != nil {
|
|
s.log.Debug("error converting dashboard to legacy dashboard", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Service) restoreVersionLegacy(
|
|
ctx context.Context, cmd *dashver.RestoreVersionCommand,
|
|
) (*dashboards.Dashboard, error) {
|
|
ctx, span := tracer.Start(ctx, "Service.restoreVersionLegacy")
|
|
defer span.End()
|
|
|
|
// We must use separate gctx context here, because it will be canceled, once the group is done.
|
|
// If we use the same ctx after the call to g.Wait, it will already be canceled at that point,
|
|
// causing all subsequent context-using operations to immediately return with "context canceled" error.
|
|
g, gctx := errgroup.WithContext(ctx)
|
|
|
|
var currentDash *dashboards.Dashboard
|
|
g.Go(func() error {
|
|
var err error
|
|
currentDash, err = s.dashSvc.GetDashboard(gctx, &dashboards.GetDashboardQuery{
|
|
UID: cmd.DashboardUID,
|
|
OrgID: cmd.Requester.GetOrgID(),
|
|
})
|
|
if err != nil {
|
|
s.log.Debug("error getting dashboard", "error", err)
|
|
}
|
|
return err
|
|
})
|
|
|
|
var versionData *dashver.DashboardVersionDTO
|
|
g.Go(func() error {
|
|
versionObj, err := s.getDashboardVersionThroughK8s(gctx, cmd.Requester.GetOrgID(), cmd.DashboardUID, cmd.Version)
|
|
if err != nil {
|
|
s.log.Debug("error getting dashboard version", "error", err)
|
|
return err
|
|
}
|
|
|
|
versionData, err = s.transformUnstructuredToLegacyDTO(gctx, versionObj)
|
|
if err != nil {
|
|
s.log.Debug("error transforming dashboard version to DTO", "error", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
if compareDashboardData(versionData.Data.MustMap(), currentDash.Data.MustMap(), true) {
|
|
return nil, dashboards.ErrDashboardRestoreIdenticalVersion
|
|
}
|
|
|
|
userID, err := identity.UserIdentifier(cmd.Requester.GetID())
|
|
if err != nil {
|
|
s.log.Debug("error getting user identifier", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
// This logic has been copied from the API handler unmodified for the most part.
|
|
// There is some strange back-and-forth conversions between the two commands,
|
|
// that should ideally be cleaned up.
|
|
saveCmd := dashboards.SaveDashboardCommand{
|
|
RestoredFrom: versionData.Version,
|
|
OrgID: cmd.Requester.GetOrgID(),
|
|
UserID: userID,
|
|
Dashboard: versionData.Data,
|
|
FolderUID: currentDash.FolderUID,
|
|
}
|
|
saveCmd.Dashboard.Set("version", currentDash.Version)
|
|
saveCmd.Dashboard.Set("uid", currentDash.UID)
|
|
dash := saveCmd.GetDashboardModel()
|
|
dashItem := &dashboards.SaveDashboardDTO{
|
|
User: cmd.Requester,
|
|
OrgID: cmd.Requester.GetOrgID(),
|
|
UpdatedAt: time.Now(),
|
|
Message: dashboardRestoreMessage(versionData.Version),
|
|
Overwrite: false,
|
|
Dashboard: dash,
|
|
}
|
|
|
|
res, err := s.dashSvc.SaveDashboard(ctx, dashItem, true)
|
|
if err != nil {
|
|
s.log.Debug("error saving dashboard", "error", err)
|
|
return nil, tracing.Error(span, err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *Service) transformUnstructuredToLegacyDTO(
|
|
ctx context.Context, item *unstructured.Unstructured,
|
|
) (*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 unstructuredToLegacyDashboardVersionWithUsers(item, users)
|
|
}
|
|
|
|
func (s *Service) transformUnstructuredToLegacyDTOList(
|
|
ctx context.Context, items []unstructured.Unstructured,
|
|
) ([]*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 := unstructuredToLegacyDashboardVersionWithUsers(&item, users)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
versions[i] = version
|
|
}
|
|
|
|
return versions, 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
|
|
}
|
|
|
|
// DashboardVersionSpec contains the necessary fields to represent a dashboard version.
|
|
type DashboardVersionSpec struct {
|
|
UID string
|
|
Version int64
|
|
ParentVersion int64
|
|
Spec any
|
|
MetaAccessor utils.GrafanaMetaAccessor
|
|
}
|
|
|
|
// UnstructuredToDashboardVersionSpec converts a k8s unstructured object to a DashboardVersionSpec.
|
|
// It supports dashboard API versions v0alpha1 through v2beta1.
|
|
func UnstructuredToDashboardVersionSpec(obj *unstructured.Unstructured, dst *DashboardVersionSpec) error {
|
|
if obj.GetAPIVersion() == v2alpha1.GroupVersion.String() ||
|
|
obj.GetAPIVersion() == v2beta1.GroupVersion.String() {
|
|
spec, ok := obj.Object["spec"]
|
|
if !ok {
|
|
return errors.New("error parsing dashboard from k8s response")
|
|
}
|
|
|
|
meta, err := utils.MetaAccessor(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
version := meta.GetGeneration()
|
|
parentVersion := version - 1
|
|
if parentVersion < 0 {
|
|
parentVersion = 0
|
|
}
|
|
|
|
dst.UID = obj.GetName()
|
|
dst.Version = version
|
|
dst.ParentVersion = parentVersion
|
|
dst.Spec = spec
|
|
dst.MetaAccessor = meta
|
|
|
|
return nil
|
|
}
|
|
|
|
// Otherwise we assume that we are dealing with a legacy dashboard API version (v0 / v1 / etc.)
|
|
|
|
spec, ok := obj.Object["spec"].(map[string]any)
|
|
if !ok {
|
|
return errors.New("error parsing dashboard from k8s response")
|
|
}
|
|
meta, err := utils.MetaAccessor(obj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
uid := meta.GetName()
|
|
spec["uid"] = uid
|
|
|
|
dashVersion := meta.GetGeneration()
|
|
parentVersion := dashVersion - 1
|
|
if parentVersion < 0 {
|
|
parentVersion = 0
|
|
}
|
|
if dashVersion > 0 {
|
|
spec["version"] = dashVersion
|
|
}
|
|
|
|
dst.UID = uid
|
|
dst.Version = dashVersion
|
|
dst.ParentVersion = parentVersion
|
|
dst.Spec = spec
|
|
dst.MetaAccessor = meta
|
|
|
|
return nil
|
|
}
|
|
|
|
func unstructuredToLegacyDashboardVersionWithUsers(
|
|
item *unstructured.Unstructured, users map[string]*user.User,
|
|
) (*dashver.DashboardVersionDTO, error) {
|
|
var vspec DashboardVersionSpec
|
|
if err := UnstructuredToDashboardVersionSpec(item, &vspec); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
obj := vspec.MetaAccessor
|
|
|
|
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: vspec.Version,
|
|
DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck
|
|
DashboardUID: vspec.UID,
|
|
Created: created,
|
|
CreatedBy: createdByID,
|
|
Message: obj.GetMessage(),
|
|
RestoredFrom: restoreVer,
|
|
Version: int(vspec.Version),
|
|
ParentVersion: int(vspec.ParentVersion),
|
|
Data: simplejson.NewFromAny(vspec.Spec),
|
|
}, nil
|
|
}
|
|
|
|
const 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
|
|
}
|
|
|
|
// compareUnstructuredDashboards compares two dashboards in unstructured.Unstructured format.
|
|
func compareUnstructuredDashboards(dst, src *unstructured.Unstructured) (bool, error) {
|
|
dstVersion := dst.GetAPIVersion()
|
|
srcVersion := src.GetAPIVersion()
|
|
|
|
// Both should have the same API version for comparison
|
|
if dstVersion != srcVersion {
|
|
return false, fmt.Errorf("cannot compare dashboards with different API versions: %s vs %s", dstVersion, srcVersion)
|
|
}
|
|
|
|
dstSpec, ok := dst.Object["spec"].(map[string]any)
|
|
if !ok {
|
|
return false, fmt.Errorf("failed to parse spec for the dashboard version")
|
|
}
|
|
|
|
srcSpec, ok := src.Object["spec"].(map[string]any)
|
|
if !ok {
|
|
return false, fmt.Errorf("failed to parse spec for the current dashboard")
|
|
}
|
|
|
|
cleanData := dstVersion == v0alpha1.APIVersion ||
|
|
dstVersion == v1beta1.APIVersion
|
|
|
|
return compareDashboardData(dstSpec, srcSpec, cleanData), nil
|
|
}
|
|
|
|
func compareDashboardData(versionData, dashData map[string]any, cleanData bool) bool {
|
|
if cleanData {
|
|
// these can be different but the actual data is the same
|
|
delete(versionData, "version")
|
|
delete(dashData, "version")
|
|
delete(versionData, "id")
|
|
delete(dashData, "id")
|
|
delete(versionData, "uid")
|
|
delete(dashData, "uid")
|
|
}
|
|
|
|
return reflect.DeepEqual(versionData, dashData)
|
|
}
|