Preferences: Support Create+Update+Delete over apiserver (#111715)

This commit is contained in:
Ryan McKinley
2025-10-02 21:58:08 +03:00
committed by GitHub
parent 5a5aac1d6b
commit 0af6efb096
20 changed files with 969 additions and 177 deletions
@@ -13,11 +13,13 @@ import (
requestK8s "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
authlib "github.com/grafana/authlib/types"
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
utilsOrig "github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
pref "github.com/grafana/grafana/pkg/services/preference"
)
var (
@@ -26,13 +28,14 @@ var (
_ rest.Getter = (*preferenceStorage)(nil)
_ rest.Lister = (*preferenceStorage)(nil)
_ rest.Storage = (*preferenceStorage)(nil)
// _ rest.Creater = (*preferenceStorage)(nil)
// _ rest.Updater = (*preferenceStorage)(nil)
// _ rest.GracefulDeleter = (*preferenceStorage)(nil)
_ rest.Creater = (*preferenceStorage)(nil)
_ rest.Updater = (*preferenceStorage)(nil)
_ rest.GracefulDeleter = (*preferenceStorage)(nil)
)
func NewPreferencesStorage(namespacer request.NamespaceMapper, sql *LegacySQL) *preferenceStorage {
func NewPreferencesStorage(pref pref.Service, namespacer request.NamespaceMapper, sql *LegacySQL) *preferenceStorage {
return &preferenceStorage{
prefs: pref,
namespacer: namespacer,
sql: sql,
tableConverter: preferences.PreferencesResourceInfo.TableConverter(),
@@ -43,6 +46,7 @@ type preferenceStorage struct {
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
sql *LegacySQL
prefs pref.Service
}
func (s *preferenceStorage) New() runtime.Object {
@@ -73,7 +77,7 @@ func (s *preferenceStorage) List(ctx context.Context, options *internalversion.L
return nil, err
}
ns := requestK8s.NamespaceValue(ctx)
if user.GetIsGrafanaAdmin() {
if user.GetIdentityType() == authlib.TypeAccessPolicy {
user = nil // nill user can see everything
}
return s.sql.ListPreferences(ctx, ns, user, true)
@@ -116,6 +120,151 @@ func (s *preferenceStorage) Get(ctx context.Context, name string, options *metav
return nil, preferences.PreferencesResourceInfo.NewNotFound(name)
}
func (s *preferenceStorage) save(ctx context.Context, obj runtime.Object) (runtime.Object, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
p, ok := obj.(*preferences.Preferences)
if !ok {
return nil, fmt.Errorf("expected preferences")
}
owner, ok := utils.ParseOwnerFromName(p.Name)
if !ok {
return nil, fmt.Errorf("invalid name")
}
cmd := &pref.SavePreferenceCommand{
OrgID: user.GetOrgID(),
HomeDashboardUID: p.Spec.HomeDashboardUID,
}
if p.Spec.Timezone != nil {
cmd.Timezone = *p.Spec.Timezone
}
if p.Spec.WeekStart != nil {
cmd.WeekStart = *p.Spec.WeekStart
}
if p.Spec.Theme != nil {
cmd.Theme = *p.Spec.Theme
}
if p.Spec.Language != nil {
cmd.Language = *p.Spec.Language
}
if p.Spec.RegionalFormat != nil {
cmd.RegionalFormat = *p.Spec.RegionalFormat
}
if p.Spec.QueryHistory != nil {
cmd.QueryHistory = &pref.QueryHistoryPreference{
HomeTab: *p.Spec.QueryHistory.HomeTab,
}
}
if p.Spec.Navbar != nil {
cmd.Navbar = &pref.NavbarPreference{
BookmarkUrls: p.Spec.Navbar.BookmarkUrls,
}
}
if p.Spec.CookiePreferences != nil {
cmd.CookiePreferences = []pref.CookieType{}
if p.Spec.CookiePreferences.Analytics != nil {
cmd.CookiePreferences = append(cmd.CookiePreferences, "analytics")
}
if p.Spec.CookiePreferences.Functional != nil {
cmd.CookiePreferences = append(cmd.CookiePreferences, "functional")
}
if p.Spec.CookiePreferences.Performance != nil {
cmd.CookiePreferences = append(cmd.CookiePreferences, "performance")
}
}
switch owner.Owner {
case utils.NamespaceResourceOwner:
// the org ID is already set
case utils.UserResourceOwner:
if user.GetIdentifier() != owner.Identifier {
return nil, fmt.Errorf("only the user can save preferences")
}
cmd.UserID, err = user.GetInternalID()
if err != nil {
return nil, err
}
case utils.TeamResourceOwner:
cmd.TeamID, err = s.sql.getLegacyTeamID(ctx, user.GetOrgID(), owner.Identifier)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported name")
}
if err = s.prefs.Save(ctx, cmd); err != nil {
return nil, err
}
return s.Get(ctx, owner.AsName(), &metav1.GetOptions{})
}
// Create implements rest.Creater.
func (s *preferenceStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
return s.save(ctx, obj)
}
// Update implements rest.Updater.
func (s *preferenceStorage) 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
}
obj, err = s.save(ctx, obj)
return obj, false, err
}
// Delete implements rest.GracefulDeleter.
func (s *preferenceStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, false, err
}
owner, ok := utils.ParseOwnerFromName(name)
if !ok {
return nil, false, fmt.Errorf("invalid name")
}
cmd := &pref.DeleteCommand{}
switch owner.Owner {
case utils.TeamResourceOwner:
cmd.TeamID, err = user.GetInternalID()
if err != nil {
return nil, false, err
}
case utils.UserResourceOwner:
cmd.UserID, err = user.GetInternalID()
if err != nil {
return nil, false, err
}
case utils.NamespaceResourceOwner:
cmd.OrgID = user.GetOrgID()
default:
return nil, false, fmt.Errorf("unsupported owner")
}
err = s.prefs.Delete(ctx, cmd)
return nil, (err == nil), err
}
func asPreferencesResource(ns string, p *preferenceModel) preferences.Preferences {
owner := utils.OwnerReference{}
if p.TeamUID.Valid {
+23 -5
View File
@@ -49,7 +49,7 @@ func NewLegacySQL(db legacysql.LegacyDatabaseProvider) *LegacySQL {
}
// NOTE: this does not support paging -- lets check if that will be a problem in cloud
func (s *LegacySQL) GetStars(ctx context.Context, orgId int64, user string) ([]dashboardStars, int64, error) {
func (s *LegacySQL) getDashboardStars(ctx context.Context, orgId int64, user string) ([]dashboardStars, int64, error) {
var max sql.NullString
sql, err := s.db(ctx)
if err != nil {
@@ -120,7 +120,10 @@ func (s *LegacySQL) GetStars(ctx context.Context, orgId int64, user string) ([]d
return nil, 0, fmt.Errorf("unable to get RV %w", err)
}
if max.Valid && max.String != "" {
fmt.Printf("max RV: %s\n", max.String)
t, _ := time.Parse(time.RFC3339, max.String)
if !t.IsZero() {
updated = t
}
} else {
updated = s.startup
}
@@ -206,7 +209,10 @@ func (s *LegacySQL) listPreferences(ctx context.Context,
return nil, 0, fmt.Errorf("unable to get RV %w", err)
}
if max.Valid && max.String != "" {
fmt.Printf("max RV: %s\n", max.String)
t, _ := time.Parse(time.RFC3339, max.String)
if !t.IsZero() {
rv.Time = t
}
} else {
rv.Time = s.startup
}
@@ -229,7 +235,7 @@ func (s *LegacySQL) ListPreferences(ctx context.Context, ns string, user identit
found, rv, err := s.listPreferences(ctx, ns, info.OrgID,
func(req *preferencesQuery) (bool, error) {
if user != nil {
req.UserUID = user.GetRawIdentifier()
req.UserUID = user.GetIdentifier()
teams, err = s.GetTeams(ctx, &identity.StaticRequester{
OrgID: info.OrgID,
UserUID: req.UserUID,
@@ -243,7 +249,7 @@ func (s *LegacySQL) ListPreferences(ctx context.Context, ns string, user identit
return true
}
if p.UserUID.String != "" {
return user.GetRawIdentifier() == p.UserUID.String
return user.GetIdentifier() == p.UserUID.String
}
if p.TeamUID.String != "" {
return slices.Contains(teams, p.TeamUID.String)
@@ -293,3 +299,15 @@ func (s *LegacySQL) GetTeams(ctx context.Context, id authlib.AuthInfo, admin boo
err = sess.Select(ctx, &teams, q, req.GetArgs()...)
return teams, err
}
func (s *LegacySQL) getLegacyTeamID(ctx context.Context, orgId int64, team string) (int64, error) {
sql, err := s.db(ctx)
if err != nil {
return 0, err
}
var id int64
sess := sql.DB.GetSqlxSession()
err = sess.Select(ctx, &id, "SELECT id FROM team WHERE org_id=? AND uid=?", orgId, team)
return id, err
}
@@ -97,13 +97,13 @@ func (s *DashboardStarsStorage) List(ctx context.Context, options *internalversi
return nil, err
}
user := userInfo.GetUID()
if userInfo.GetIsGrafanaAdmin() || userInfo.GetIdentityType() == authlib.TypeAccessPolicy {
user := userInfo.GetIdentifier()
if userInfo.GetIdentityType() == authlib.TypeAccessPolicy {
user = "" // can see everything
}
list := &preferences.StarsList{}
found, rv, err := s.sql.GetStars(ctx, ns.OrgID, user)
found, rv, err := s.sql.getDashboardStars(ctx, ns.OrgID, user)
if err != nil {
return nil, err
}
@@ -137,7 +137,7 @@ func (s *DashboardStarsStorage) Get(ctx context.Context, name string, options *m
return nil, err
}
found, _, err := s.sql.GetStars(ctx, ns.OrgID, owner.Identifier)
found, _, err := s.sql.getDashboardStars(ctx, ns.OrgID, owner.Identifier)
if err != nil {
return nil, err
}
@@ -187,7 +187,7 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star
}}, err
}
current, _, err := s.sql.GetStars(ctx, ns.OrgID, owner.Identifier)
current, _, err := s.sql.getDashboardStars(ctx, ns.OrgID, owner.Identifier)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -72,7 +72,7 @@ func RegisterAPIService(
namespacer := request.GetNamespaceMapper(cfg)
if prefs != nil {
builder.legacyPrefs = legacy.NewPreferencesStorage(namespacer, sql)
builder.legacyPrefs = legacy.NewPreferencesStorage(prefs, namespacer, sql)
}
if stars != nil {
builder.legacyStars = legacy.NewDashboardStarsStorage(stars, users, namespacer, sql)