grafana/pkg/registry/apis/preferences/legacy/stars.go

350 lines
9.9 KiB
Go

package legacy
import (
"context"
"fmt"
"math/rand"
"slices"
"strconv"
"strings"
"time"
apiserrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/utils/ptr"
authlib "github.com/grafana/authlib/types"
dashboardsV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/user"
)
var (
_ rest.Scoper = (*DashboardStarsStorage)(nil)
_ rest.SingularNameProvider = (*DashboardStarsStorage)(nil)
_ rest.Getter = (*DashboardStarsStorage)(nil)
_ rest.Lister = (*DashboardStarsStorage)(nil)
_ rest.Storage = (*DashboardStarsStorage)(nil)
_ rest.Creater = (*DashboardStarsStorage)(nil)
_ rest.Updater = (*DashboardStarsStorage)(nil)
_ rest.GracefulDeleter = (*DashboardStarsStorage)(nil)
_ rest.CollectionDeleter = (*DashboardStarsStorage)(nil)
)
func NewDashboardStarsStorage(
stars star.Service,
users user.Service,
namespacer request.NamespaceMapper,
sql *LegacySQL,
) *DashboardStarsStorage {
return &DashboardStarsStorage{
stars: stars,
users: users,
namespacer: namespacer,
sql: sql,
tableConverter: preferences.StarsResourceInfo.TableConverter(),
}
}
type DashboardStarsStorage struct {
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
sql *LegacySQL
stars star.Service
users user.Service
}
func (s *DashboardStarsStorage) New() runtime.Object {
return preferences.StarsKind().ZeroValue()
}
func (s *DashboardStarsStorage) Destroy() {}
func (s *DashboardStarsStorage) NamespaceScoped() bool {
return true // namespace == org
}
func (s *DashboardStarsStorage) GetSingularName() string {
return strings.ToLower(preferences.StarsKind().Kind())
}
func (s *DashboardStarsStorage) NewList() runtime.Object {
return preferences.StarsKind().ZeroListValue()
}
func (s *DashboardStarsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *DashboardStarsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
ns, err := request.NamespaceInfoFrom(ctx, false)
if err != nil {
return nil, err
}
if ns.Value == "" {
return nil, fmt.Errorf("cross cluster listing is not supported")
}
userInfo, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
user := userInfo.GetIdentifier()
if userInfo.GetIdentityType() == authlib.TypeAccessPolicy {
user = "" // can see everything
}
list := &preferences.StarsList{}
found, rv, err := s.sql.getDashboardStars(ctx, ns.OrgID, user)
if err != nil {
return nil, err
}
history, err := s.sql.getHistoryStars(ctx, ns.OrgID, "")
if err != nil {
return nil, err
}
for _, v := range found {
list.Items = append(list.Items,
asStarsResource(s.namespacer(v.OrgID), &v, history[v.UserUID]))
}
if rv > 0 {
list.ResourceVersion = strconv.FormatInt(rv, 10)
}
return list, nil
}
func getNamespaceAndOwner(ctx context.Context, name string) (authlib.NamespaceInfo, utils.OwnerReference, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return info, utils.OwnerReference{}, err
}
owner, ok := utils.ParseOwnerFromName(name)
if !ok {
return info, owner, fmt.Errorf("invalid name %w", err)
}
if owner.Owner != utils.UserResourceOwner {
return info, owner, fmt.Errorf("expecting name with prefix: %s-", utils.UserResourceOwner)
}
return info, owner, nil
}
func (s *DashboardStarsStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
ns, owner, err := getNamespaceAndOwner(ctx, name)
if err != nil {
return nil, err
}
found, _, err := s.sql.getDashboardStars(ctx, ns.OrgID, owner.Identifier)
if err != nil {
return nil, err
}
history, err := s.sql.getHistoryStars(ctx, ns.OrgID, owner.Identifier)
if err != nil {
return nil, err
}
if len(found) == 0 || len(found[0].Dashboards) == 0 {
return nil, apiserrors.NewNotFound(preferences.StarsResourceInfo.GroupResource(), name)
}
obj := asStarsResource(ns.Value, &found[0], history[owner.Identifier])
return &obj, nil
}
func getStars(stars *preferences.Stars, gk schema.GroupKind) []string {
if stars == nil || len(stars.Spec.Resource) == 0 {
return []string{}
}
for _, r := range stars.Spec.Resource {
if r.Group == gk.Group && r.Kind == gk.Kind {
return r.Names
}
}
return []string{}
}
// Create implements rest.Creater.
func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Stars) (runtime.Object, error) {
ns, owner, err := getNamespaceAndOwner(ctx, obj.Name)
if err != nil {
return nil, err
}
user, err := s.users.GetByUID(ctx, &user.GetUserByUIDQuery{
UID: owner.Identifier,
})
if err != nil {
return nil, err
}
if user.OrgID != ns.OrgID {
return nil, fmt.Errorf("namespace mismatch")
}
stars := getStars(obj, schema.GroupKind{Group: "dashboard.grafana.app", Kind: "Dashboard"})
if len(stars) == 0 {
err = s.stars.DeleteByUser(ctx, user.ID)
return &preferences.Stars{ObjectMeta: metav1.ObjectMeta{
Name: obj.Name,
Namespace: obj.Namespace,
DeletionTimestamp: ptr.To(metav1.Now()),
}}, err
}
current, _, err := s.sql.getDashboardStars(ctx, ns.OrgID, owner.Identifier)
if err != nil {
return nil, err
}
changed := false
now := time.Now()
randID := now.UnixNano() + rand.Int63n(5000)
previous := make(map[string]bool)
if len(current) > 0 {
for _, v := range current[0].Dashboards {
previous[v] = true
}
}
for _, dashboard := range stars {
if previous[dashboard] {
delete(previous, dashboard)
continue // nothing needed
}
err = s.stars.Add(ctx, &star.StarDashboardCommand{
UserID: user.ID,
OrgID: user.OrgID,
DashboardUID: dashboard,
DashboardID: randID,
Updated: now,
})
if err != nil {
return nil, err
}
changed = true
randID++
}
for k := range previous {
err = s.stars.Delete(ctx, &star.UnstarDashboardCommand{
UserID: user.ID,
OrgID: user.OrgID,
DashboardUID: k,
})
if err != nil {
return nil, err
}
changed = true
}
// Apply history stars
stars = getStars(obj, schema.GroupKind{Group: "history.grafana.app", Kind: "Query"})
res, err := s.sql.getHistoryStars(ctx, user.OrgID, user.UID)
if err != nil {
return nil, err
}
history := res[user.UID]
if !slices.Equal(stars, history) {
changed = true
if len(stars) == 0 {
err = s.sql.removeHistoryStar(ctx, user, nil)
if err != nil {
return nil, err
}
} else {
added, removed, _ := preferences.Changes(history, stars)
if len(removed) > 0 {
_ = s.sql.removeHistoryStar(ctx, user, nil)
}
for _, v := range added {
_ = s.sql.addHistoryStar(ctx, user, v) // one at a time so duplicates do not fail everything
}
}
}
if changed {
return s.Get(ctx, obj.Name, &metav1.GetOptions{})
}
return obj, nil // nothing required
}
// Create implements rest.Creater.
func (s *DashboardStarsStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
stars, ok := obj.(*preferences.Stars)
if !ok {
return nil, fmt.Errorf("expected stars object")
}
return s.write(ctx, stars)
}
// Update implements rest.Updater.
func (s *DashboardStarsStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
old, err := s.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
obj, err := objInfo.UpdatedObject(ctx, old)
if err != nil {
return nil, false, err
}
stars, ok := obj.(*preferences.Stars)
if !ok {
return nil, false, fmt.Errorf("expected stars object")
}
obj, err = s.write(ctx, stars)
return obj, false, err
}
// Delete implements rest.GracefulDeleter.
func (s *DashboardStarsStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
obj, err := s.write(ctx, &preferences.Stars{ObjectMeta: metav1.ObjectMeta{Name: name}})
if err != nil {
return nil, false, err
}
return obj, true, err
}
// DeleteCollection implements rest.CollectionDeleter.
func (s *DashboardStarsStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
return nil, fmt.Errorf("not implemented yet")
}
func asStarsResource(ns string, v *dashboardStars, history []string) preferences.Stars {
stars := preferences.Stars{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("user-%s", v.UserUID),
Namespace: ns,
ResourceVersion: strconv.FormatInt(v.Last, 10),
CreationTimestamp: metav1.NewTime(time.UnixMilli(v.First)),
},
Spec: preferences.StarsSpec{
Resource: []preferences.StarsResource{{
Group: dashboardsV1.APIGroup,
Kind: "Dashboard",
Names: v.Dashboards,
}},
},
}
if len(history) > 0 {
stars.Spec.Resource = append(stars.Spec.Resource, preferences.StarsResource{
Group: "history.grafana.app",
Kind: "Query",
Names: history,
})
}
stars.Spec.Normalize()
return stars
}