Stars: Move stars from preferences apiserver to a new collections apiserver (#114006)
This commit is contained in:
@@ -158,7 +158,8 @@ var serviceIdentityTokenPermissions = []string{
|
||||
"secret.grafana.app:*",
|
||||
"query.grafana.app:*",
|
||||
"iam.grafana.app:*",
|
||||
"preferences.grafana.app:*",
|
||||
"preferences.grafana.app:*", // user, team, and org preferences
|
||||
"collections.grafana.app:*", // user stars
|
||||
|
||||
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
|
||||
"secret.grafana.app/securevalues:decrypt",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apiregistry
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections"
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
@@ -27,6 +28,7 @@ func ProvideRegistryServiceSink(
|
||||
_ *query.QueryAPIBuilder,
|
||||
_ *userstorage.UserStorageAPIBuilder,
|
||||
_ *preferences.APIBuilder,
|
||||
_ *collections.APIBuilder,
|
||||
_ *provisioning.APIBuilder,
|
||||
_ *ofrep.APIBuilder,
|
||||
_ *secret.DependencyRegisterer,
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package preferences
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
|
||||
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
)
|
||||
|
||||
func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
@@ -24,7 +24,7 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis
|
||||
|
||||
switch a.GetResource().Resource {
|
||||
case "stars":
|
||||
stars, ok := obj.(*preferences.Stars)
|
||||
stars, ok := obj.(*collections.Stars)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected stars object: (%T)", obj)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
|
||||
// Templates setup.
|
||||
var (
|
||||
//go:embed *.sql
|
||||
sqlTemplatesFS embed.FS
|
||||
|
||||
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`))
|
||||
)
|
||||
|
||||
func mustTemplate(filename string) *template.Template {
|
||||
if t := sqlTemplates.Lookup(filename); t != nil {
|
||||
return t
|
||||
}
|
||||
panic(fmt.Sprintf("template file not found: %s", filename))
|
||||
}
|
||||
|
||||
// Templates.
|
||||
var (
|
||||
sqlDashboardStarsQuery = mustTemplate("sql_dashboard_stars.sql")
|
||||
sqlDashboardStarsRV = mustTemplate("sql_dashboard_stars_rv.sql")
|
||||
)
|
||||
|
||||
type starQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
|
||||
OrgID int64 // >= 1 if UserID != ""
|
||||
UserUID string
|
||||
UserID int64 // for stars
|
||||
QueryUIDs []string
|
||||
QueryUID string
|
||||
|
||||
StarTable string
|
||||
UserTable string
|
||||
QueryHistoryStarsTable string
|
||||
QueryHistoryTable string
|
||||
}
|
||||
|
||||
func (r starQuery) Validate() error {
|
||||
if r.UserUID != "" && r.OrgID < 1 {
|
||||
return fmt.Errorf("requests with a userid, must include an orgID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newStarQueryReq(sql *legacysql.LegacyDatabaseHelper, user string, orgId int64) starQuery {
|
||||
return starQuery{
|
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||
|
||||
UserUID: user,
|
||||
OrgID: orgId,
|
||||
|
||||
StarTable: sql.Table("star"),
|
||||
UserTable: sql.Table("user"),
|
||||
QueryHistoryStarsTable: sql.Table("query_history_star"),
|
||||
QueryHistoryTable: sql.Table("query_history"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
|
||||
)
|
||||
|
||||
func TestStarsQueries(t *testing.T) {
|
||||
// prefix tables with grafana
|
||||
nodb := &legacysql.LegacyDatabaseHelper{
|
||||
Table: func(n string) string {
|
||||
return "grafana." + n
|
||||
},
|
||||
}
|
||||
|
||||
getStarQuery := func(orgId int64, user string) sqltemplate.SQLTemplate {
|
||||
v := newStarQueryReq(nodb, user, orgId)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
|
||||
RootDir: "testdata",
|
||||
SQLTemplatesFS: sqlTemplatesFS,
|
||||
Templates: map[*template.Template][]mocks.TemplateTestCase{
|
||||
sqlDashboardStarsQuery: {
|
||||
{
|
||||
Name: "all",
|
||||
Data: getStarQuery(0, ""),
|
||||
},
|
||||
{
|
||||
Name: "org",
|
||||
Data: getStarQuery(3, ""),
|
||||
},
|
||||
{
|
||||
Name: "user",
|
||||
Data: getStarQuery(3, "abc"),
|
||||
},
|
||||
},
|
||||
sqlDashboardStarsRV: {
|
||||
{
|
||||
Name: "get",
|
||||
Data: getStarQuery(0, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package legacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
|
||||
type dashboardStars struct {
|
||||
OrgID int64
|
||||
UserUID string
|
||||
First int64
|
||||
Last int64
|
||||
|
||||
Dashboards []string
|
||||
}
|
||||
|
||||
type LegacySQL struct {
|
||||
db legacysql.LegacyDatabaseProvider
|
||||
startup time.Time
|
||||
}
|
||||
|
||||
func NewLegacySQL(db legacysql.LegacyDatabaseProvider) *LegacySQL {
|
||||
return &LegacySQL{db: db, startup: time.Now()}
|
||||
}
|
||||
|
||||
// NOTE: this does not support paging -- lets check if that will be a problem in cloud
|
||||
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 {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
req := newStarQueryReq(sql, user, orgId)
|
||||
|
||||
q, err := sqltemplate.Execute(sqlDashboardStarsQuery, req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("execute template %q: %w", sqlDashboardStarsQuery.Name(), err)
|
||||
}
|
||||
|
||||
sess := sql.DB.GetSqlxSession()
|
||||
rows, err := sess.Query(ctx, q, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() {
|
||||
if rows != nil {
|
||||
_ = rows.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
stars := []dashboardStars{}
|
||||
current := &dashboardStars{}
|
||||
var orgID int64
|
||||
var userUID string
|
||||
var dashboardUID string
|
||||
var updated time.Time
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&orgID, &userUID, &dashboardUID, &updated)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if orgID != current.OrgID || userUID != current.UserUID {
|
||||
if current.UserUID != "" {
|
||||
stars = append(stars, *current)
|
||||
}
|
||||
current = &dashboardStars{
|
||||
OrgID: orgID,
|
||||
UserUID: userUID,
|
||||
}
|
||||
}
|
||||
ts := updated.UnixMilli()
|
||||
if ts > current.Last {
|
||||
current.Last = ts
|
||||
}
|
||||
if ts < current.First || current.First == 0 {
|
||||
current.First = ts
|
||||
}
|
||||
current.Dashboards = append(current.Dashboards, dashboardUID)
|
||||
}
|
||||
|
||||
// Add the last value
|
||||
if current.UserUID != "" {
|
||||
stars = append(stars, *current)
|
||||
}
|
||||
|
||||
// Find the RV unless it is a user query
|
||||
if userUID == "" {
|
||||
req.Reset()
|
||||
q, err = sqltemplate.Execute(sqlDashboardStarsRV, req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("execute template %q: %w", sqlDashboardStarsRV.Name(), err)
|
||||
}
|
||||
err = sess.Get(ctx, &max, q)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("unable to get RV %w", err)
|
||||
}
|
||||
if max.Valid && max.String != "" {
|
||||
t, _ := time.Parse(time.RFC3339, max.String)
|
||||
if !t.IsZero() {
|
||||
updated = t
|
||||
}
|
||||
} else {
|
||||
updated = s.startup
|
||||
}
|
||||
}
|
||||
|
||||
return stars, updated.UnixMilli(), err
|
||||
}
|
||||
+19
-61
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -18,8 +17,8 @@ import (
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
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"
|
||||
@@ -50,7 +49,7 @@ func NewDashboardStarsStorage(
|
||||
users: users,
|
||||
namespacer: namespacer,
|
||||
sql: sql,
|
||||
tableConverter: preferences.StarsResourceInfo.TableConverter(),
|
||||
tableConverter: collections.StarsResourceInfo.TableConverter(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +62,7 @@ type DashboardStarsStorage struct {
|
||||
}
|
||||
|
||||
func (s *DashboardStarsStorage) New() runtime.Object {
|
||||
return preferences.StarsKind().ZeroValue()
|
||||
return collections.StarsKind().ZeroValue()
|
||||
}
|
||||
|
||||
func (s *DashboardStarsStorage) Destroy() {}
|
||||
@@ -73,11 +72,11 @@ func (s *DashboardStarsStorage) NamespaceScoped() bool {
|
||||
}
|
||||
|
||||
func (s *DashboardStarsStorage) GetSingularName() string {
|
||||
return strings.ToLower(preferences.StarsKind().Kind())
|
||||
return strings.ToLower(collections.StarsKind().Kind())
|
||||
}
|
||||
|
||||
func (s *DashboardStarsStorage) NewList() runtime.Object {
|
||||
return preferences.StarsKind().ZeroListValue()
|
||||
return collections.StarsKind().ZeroListValue()
|
||||
}
|
||||
|
||||
func (s *DashboardStarsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
@@ -104,18 +103,14 @@ func (s *DashboardStarsStorage) List(ctx context.Context, options *internalversi
|
||||
user = "" // can see everything
|
||||
}
|
||||
|
||||
list := &preferences.StarsList{}
|
||||
list := &collections.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]))
|
||||
asStarsResource(s.namespacer(v.OrgID), &v))
|
||||
}
|
||||
if rv > 0 {
|
||||
list.ResourceVersion = strconv.FormatInt(rv, 10)
|
||||
@@ -149,19 +144,14 @@ func (s *DashboardStarsStorage) Get(ctx context.Context, name string, options *m
|
||||
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)
|
||||
return nil, apiserrors.NewNotFound(collections.StarsResourceInfo.GroupResource(), name)
|
||||
}
|
||||
obj := asStarsResource(ns.Value, &found[0], history[owner.Identifier])
|
||||
obj := asStarsResource(ns.Value, &found[0])
|
||||
return &obj, nil
|
||||
}
|
||||
|
||||
func getStars(stars *preferences.Stars, gk schema.GroupKind) []string {
|
||||
func getStars(stars *collections.Stars, gk schema.GroupKind) []string {
|
||||
if stars == nil || len(stars.Spec.Resource) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
@@ -174,7 +164,7 @@ func getStars(stars *preferences.Stars, gk schema.GroupKind) []string {
|
||||
}
|
||||
|
||||
// Create implements rest.Creater.
|
||||
func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Stars) (runtime.Object, error) {
|
||||
func (s *DashboardStarsStorage) write(ctx context.Context, obj *collections.Stars) (runtime.Object, error) {
|
||||
ns, owner, err := getNamespaceAndOwner(ctx, obj.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -193,7 +183,7 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star
|
||||
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{
|
||||
return &collections.Stars{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: obj.Name,
|
||||
Namespace: obj.Namespace,
|
||||
DeletionTimestamp: ptr.To(metav1.Now()),
|
||||
@@ -245,31 +235,6 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star
|
||||
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{})
|
||||
}
|
||||
@@ -278,7 +243,7 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star
|
||||
|
||||
// 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)
|
||||
stars, ok := obj.(*collections.Stars)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected stars object")
|
||||
}
|
||||
@@ -298,7 +263,7 @@ func (s *DashboardStarsStorage) Update(ctx context.Context, name string, objInfo
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
stars, ok := obj.(*preferences.Stars)
|
||||
stars, ok := obj.(*collections.Stars)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("expected stars object")
|
||||
}
|
||||
@@ -309,7 +274,7 @@ func (s *DashboardStarsStorage) Update(ctx context.Context, name string, objInfo
|
||||
|
||||
// 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}})
|
||||
obj, err := s.write(ctx, &collections.Stars{ObjectMeta: metav1.ObjectMeta{Name: name}})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -321,29 +286,22 @@ func (s *DashboardStarsStorage) DeleteCollection(ctx context.Context, deleteVali
|
||||
return nil, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
func asStarsResource(ns string, v *dashboardStars, history []string) preferences.Stars {
|
||||
stars := preferences.Stars{
|
||||
func asStarsResource(ns string, v *dashboardStars) collections.Stars {
|
||||
stars := collections.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{{
|
||||
Spec: collections.StarsSpec{
|
||||
Resource: []collections.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
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/star"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
)
|
||||
|
||||
var (
|
||||
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
||||
_ builder.APIGroupMutation = (*APIBuilder)(nil)
|
||||
)
|
||||
|
||||
type APIBuilder struct {
|
||||
authorizer authorizer.Authorizer
|
||||
legacyStars *legacy.DashboardStarsStorage
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
db db.DB,
|
||||
stars star.Service,
|
||||
users user.Service,
|
||||
apiregistration builder.APIRegistrar,
|
||||
) *APIBuilder {
|
||||
// Requires development settings and clearly experimental
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil
|
||||
}
|
||||
|
||||
sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db))
|
||||
builder := &APIBuilder{
|
||||
authorizer: &utils.AuthorizeFromName{
|
||||
Resource: map[string][]utils.ResourceOwner{
|
||||
"stars": {utils.UserResourceOwner},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
if stars != nil {
|
||||
builder.legacyStars = legacy.NewDashboardStarsStorage(stars, users, namespacer, sql)
|
||||
}
|
||||
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
// AllowedV0Alpha1Resources implements builder.APIGroupBuilder.
|
||||
func (b *APIBuilder) AllowedV0Alpha1Resources() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *APIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||
return collections.GroupVersion
|
||||
}
|
||||
|
||||
func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
gv := collections.GroupVersion
|
||||
err := collections.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metav1.AddToGroupVersion(scheme, gv)
|
||||
return scheme.SetVersionPriority(gv)
|
||||
}
|
||||
|
||||
func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
// Configure Stars Dual writer
|
||||
resource := collections.StarsResourceInfo
|
||||
var stars grafanarest.Storage
|
||||
stars, err := grafanaregistry.NewRegistryStore(opts.Scheme, resource, opts.OptsGetter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stars = &starStorage{Storage: stars} // wrap List so we only return one value
|
||||
if b.legacyStars != nil && opts.DualWriteBuilder != nil {
|
||||
stars, err = opts.DualWriteBuilder(resource.GroupResource(), b.legacyStars, stars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
storage[resource.StoragePath()] = stars
|
||||
storage[resource.StoragePath("update")] = &starsREST{store: stars}
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[collections.APIVersion] = storage
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return b.authorizer
|
||||
}
|
||||
|
||||
func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||
return collections.GetOpenAPIDefinitions
|
||||
}
|
||||
|
||||
func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
oas.Info.Description = "Grafana collections"
|
||||
|
||||
root := "/apis/" + b.GetGroupVersion().String() + "/"
|
||||
updateKey := root + "namespaces/{namespace}/stars/{name}/update"
|
||||
delete(oas.Paths.Paths, updateKey)
|
||||
|
||||
// Add the group/kind/id properties to the path
|
||||
stars, ok := oas.Paths.Paths[updateKey+"/{path}"]
|
||||
if !ok || stars == nil {
|
||||
return nil, fmt.Errorf("unable to find write path")
|
||||
}
|
||||
stars.Parameters = []*spec3.Parameter{
|
||||
stars.Parameters[0], // name
|
||||
stars.Parameters[1], // namespace
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "group",
|
||||
In: "path",
|
||||
Example: "dashboard.grafana.app",
|
||||
Description: "API group for stared item",
|
||||
Schema: spec.StringProperty(),
|
||||
Required: true,
|
||||
},
|
||||
}, {
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "kind",
|
||||
In: "path",
|
||||
Example: "Dashboard",
|
||||
Description: "Kind for stared item",
|
||||
Schema: spec.StringProperty(),
|
||||
Required: true,
|
||||
},
|
||||
}, {
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "id",
|
||||
In: "path",
|
||||
Example: "",
|
||||
Description: "The k8s name for the selected item",
|
||||
Schema: spec.StringProperty(),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
stars.Put.Description = "Add a starred item"
|
||||
stars.Put.OperationId = "addStar"
|
||||
stars.Delete.Description = "Remove a starred item"
|
||||
stars.Delete.OperationId = "removeStar"
|
||||
|
||||
delete(oas.Paths.Paths, updateKey+"/{path}")
|
||||
oas.Paths.Paths[updateKey+"/{group}/{kind}/{id}"] = stars
|
||||
|
||||
return oas, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package preferences
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
)
|
||||
@@ -32,12 +32,12 @@ func (s *starStorage) List(ctx context.Context, options *internalversion.ListOpt
|
||||
|
||||
// Get the single user stars
|
||||
case authlib.TypeUser:
|
||||
stars := &preferences.StarsList{}
|
||||
stars := &collections.StarsList{}
|
||||
obj, _ := s.Get(ctx, "user-"+user.GetIdentifier(), &v1.GetOptions{})
|
||||
if obj != nil {
|
||||
s, ok := obj.(*preferences.Stars)
|
||||
s, ok := obj.(*collections.Stars)
|
||||
if ok {
|
||||
stars.Items = []preferences.Stars{*s}
|
||||
stars.Items = []collections.Stars{*s}
|
||||
}
|
||||
}
|
||||
return stars, nil
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package preferences
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
|
||||
@@ -33,7 +33,7 @@ var (
|
||||
)
|
||||
|
||||
func (r *starsREST) New() runtime.Object {
|
||||
return &preferences.Stars{}
|
||||
return &collections.Stars{}
|
||||
}
|
||||
|
||||
func (r *starsREST) Destroy() {}
|
||||
@@ -47,7 +47,7 @@ func (r *starsREST) ProducesMIMETypes(verb string) []string {
|
||||
}
|
||||
|
||||
func (r *starsREST) ProducesObject(verb string) interface{} {
|
||||
return &preferences.Stars{}
|
||||
return &collections.Stars{}
|
||||
}
|
||||
|
||||
func (r *starsREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
@@ -81,7 +81,7 @@ func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object,
|
||||
case "PUT":
|
||||
remove = false
|
||||
default:
|
||||
responder.Error(apierrors.NewMethodNotSupported(preferences.PreferencesResourceInfo.GroupResource(), req.Method))
|
||||
responder.Error(apierrors.NewMethodNotSupported(collections.StarsResourceInfo.GroupResource(), req.Method))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object,
|
||||
})
|
||||
return
|
||||
}
|
||||
current = &preferences.Stars{
|
||||
current = &collections.Stars{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: user.GetNamespace(),
|
||||
@@ -103,7 +103,7 @@ func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object,
|
||||
}
|
||||
}
|
||||
|
||||
obj, ok := current.(*preferences.Stars)
|
||||
obj, ok := current.(*collections.Stars)
|
||||
if !ok {
|
||||
responder.Error(fmt.Errorf("expected stars object"))
|
||||
return
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package preferences
|
||||
package collections
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -16,7 +16,7 @@ func TestStarsWrite(t *testing.T) {
|
||||
err string
|
||||
}{{
|
||||
name: "normal",
|
||||
url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127",
|
||||
url: "http://localhost:3000/apis/collections.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127",
|
||||
prefix: "/user-abc/write",
|
||||
item: starItem{
|
||||
group: "dashboard.grafana.app",
|
||||
@@ -25,12 +25,12 @@ func TestStarsWrite(t *testing.T) {
|
||||
},
|
||||
}, {
|
||||
name: "prefix not found",
|
||||
url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127",
|
||||
url: "http://localhost:3000/apis/collections.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127",
|
||||
prefix: "/something/write",
|
||||
err: "invalid request path",
|
||||
}, {
|
||||
name: "missing three parts",
|
||||
url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/000000127",
|
||||
url: "http://localhost:3000/apis/collections.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/000000127",
|
||||
prefix: "/user-abc/write",
|
||||
err: "expected {group}/{kind}/{id}",
|
||||
}}
|
||||
@@ -26,52 +26,11 @@ func mustTemplate(filename string) *template.Template {
|
||||
|
||||
// Templates.
|
||||
var (
|
||||
sqlDashboardStarsQuery = mustTemplate("sql_dashboard_stars.sql")
|
||||
sqlDashboardStarsRV = mustTemplate("sql_dashboard_stars_rv.sql")
|
||||
sqlHistoryStarsQuery = mustTemplate("sql_history_stars.sql")
|
||||
sqlHistoryStarsInsert = mustTemplate("sql_history_stars_insert.sql")
|
||||
sqlHistoryStarsDelete = mustTemplate("sql_history_stars_delete.sql")
|
||||
sqlPreferencesQuery = mustTemplate("sql_preferences_query.sql")
|
||||
sqlPreferencesRV = mustTemplate("sql_preferences_rv.sql")
|
||||
sqlTeams = mustTemplate("sql_teams.sql")
|
||||
sqlPreferencesQuery = mustTemplate("sql_preferences_query.sql")
|
||||
sqlPreferencesRV = mustTemplate("sql_preferences_rv.sql")
|
||||
sqlTeams = mustTemplate("sql_teams.sql")
|
||||
)
|
||||
|
||||
type starQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
|
||||
OrgID int64 // >= 1 if UserID != ""
|
||||
UserUID string
|
||||
UserID int64 // for stars
|
||||
QueryUIDs []string
|
||||
QueryUID string
|
||||
|
||||
StarTable string
|
||||
UserTable string
|
||||
QueryHistoryStarsTable string
|
||||
QueryHistoryTable string
|
||||
}
|
||||
|
||||
func (r starQuery) Validate() error {
|
||||
if r.UserUID != "" && r.OrgID < 1 {
|
||||
return fmt.Errorf("requests with a userid, must include an orgID")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newStarQueryReq(sql *legacysql.LegacyDatabaseHelper, user string, orgId int64) starQuery {
|
||||
return starQuery{
|
||||
SQLTemplate: sqltemplate.New(sql.DialectForDriver()),
|
||||
|
||||
UserUID: user,
|
||||
OrgID: orgId,
|
||||
|
||||
StarTable: sql.Table("star"),
|
||||
UserTable: sql.Table("user"),
|
||||
QueryHistoryStarsTable: sql.Table("query_history_star"),
|
||||
QueryHistoryTable: sql.Table("query_history"),
|
||||
}
|
||||
}
|
||||
|
||||
type preferencesQuery struct {
|
||||
sqltemplate.SQLTemplate
|
||||
|
||||
|
||||
@@ -17,21 +17,6 @@ func TestStarsQueries(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
getStarQuery := func(orgId int64, user string) sqltemplate.SQLTemplate {
|
||||
v := newStarQueryReq(nodb, user, orgId)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
getHistoryReq := func(orgId int64, userId int64, stars []string, star string) sqltemplate.SQLTemplate {
|
||||
v := newStarQueryReq(nodb, "", orgId)
|
||||
v.UserID = userId
|
||||
v.QueryUIDs = stars
|
||||
v.QueryUID = star
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
return &v
|
||||
}
|
||||
|
||||
getPreferencesQuery := func(orgId int64, cb func(q *preferencesQuery)) sqltemplate.SQLTemplate {
|
||||
v := newPreferencesQueryReq(nodb, orgId)
|
||||
v.SQLTemplate = mocks.NewTestingSQLTemplate()
|
||||
@@ -49,56 +34,6 @@ func TestStarsQueries(t *testing.T) {
|
||||
RootDir: "testdata",
|
||||
SQLTemplatesFS: sqlTemplatesFS,
|
||||
Templates: map[*template.Template][]mocks.TemplateTestCase{
|
||||
sqlDashboardStarsQuery: {
|
||||
{
|
||||
Name: "all",
|
||||
Data: getStarQuery(0, ""),
|
||||
},
|
||||
{
|
||||
Name: "org",
|
||||
Data: getStarQuery(3, ""),
|
||||
},
|
||||
{
|
||||
Name: "user",
|
||||
Data: getStarQuery(3, "abc"),
|
||||
},
|
||||
},
|
||||
sqlDashboardStarsRV: {
|
||||
{
|
||||
Name: "get",
|
||||
Data: getStarQuery(0, ""),
|
||||
},
|
||||
},
|
||||
sqlHistoryStarsQuery: {
|
||||
{
|
||||
Name: "user",
|
||||
Data: getStarQuery(1, "abc"),
|
||||
},
|
||||
},
|
||||
sqlHistoryStarsQuery: {
|
||||
{
|
||||
Name: "org",
|
||||
Data: getStarQuery(1, ""),
|
||||
},
|
||||
},
|
||||
sqlHistoryStarsInsert: {
|
||||
{
|
||||
Name: "add star",
|
||||
Data: getHistoryReq(1, 3, nil, "XXX"),
|
||||
},
|
||||
},
|
||||
sqlHistoryStarsDelete: {
|
||||
{
|
||||
Name: "remove star",
|
||||
Data: getHistoryReq(1, 3, []string{"xxx", "yyy"}, ""),
|
||||
},
|
||||
},
|
||||
sqlHistoryStarsDelete: {
|
||||
{
|
||||
Name: "remove all star",
|
||||
Data: getHistoryReq(1, 3, nil, ""),
|
||||
},
|
||||
},
|
||||
sqlPreferencesQuery: {
|
||||
{
|
||||
Name: "all",
|
||||
|
||||
@@ -12,20 +12,10 @@ import (
|
||||
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
)
|
||||
|
||||
type dashboardStars struct {
|
||||
OrgID int64
|
||||
UserUID string
|
||||
First int64
|
||||
Last int64
|
||||
|
||||
Dashboards []string
|
||||
}
|
||||
|
||||
type preferenceModel struct {
|
||||
ID int64
|
||||
OrgID int64
|
||||
@@ -49,177 +39,6 @@ func NewLegacySQL(db legacysql.LegacyDatabaseProvider) *LegacySQL {
|
||||
return &LegacySQL{db: db, startup: time.Now()}
|
||||
}
|
||||
|
||||
// NOTE: this does not support paging -- lets check if that will be a problem in cloud
|
||||
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 {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
req := newStarQueryReq(sql, user, orgId)
|
||||
|
||||
q, err := sqltemplate.Execute(sqlDashboardStarsQuery, req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("execute template %q: %w", sqlDashboardStarsQuery.Name(), err)
|
||||
}
|
||||
|
||||
sess := sql.DB.GetSqlxSession()
|
||||
rows, err := sess.Query(ctx, q, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() {
|
||||
if rows != nil {
|
||||
_ = rows.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
stars := []dashboardStars{}
|
||||
current := &dashboardStars{}
|
||||
var orgID int64
|
||||
var userUID string
|
||||
var dashboardUID string
|
||||
var updated time.Time
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&orgID, &userUID, &dashboardUID, &updated)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if orgID != current.OrgID || userUID != current.UserUID {
|
||||
if current.UserUID != "" {
|
||||
stars = append(stars, *current)
|
||||
}
|
||||
current = &dashboardStars{
|
||||
OrgID: orgID,
|
||||
UserUID: userUID,
|
||||
}
|
||||
}
|
||||
ts := updated.UnixMilli()
|
||||
if ts > current.Last {
|
||||
current.Last = ts
|
||||
}
|
||||
if ts < current.First || current.First == 0 {
|
||||
current.First = ts
|
||||
}
|
||||
current.Dashboards = append(current.Dashboards, dashboardUID)
|
||||
}
|
||||
|
||||
// Add the last value
|
||||
if current.UserUID != "" {
|
||||
stars = append(stars, *current)
|
||||
}
|
||||
|
||||
// Find the RV unless it is a user query
|
||||
if userUID == "" {
|
||||
req.Reset()
|
||||
q, err = sqltemplate.Execute(sqlDashboardStarsRV, req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("execute template %q: %w", sqlPreferencesRV.Name(), err)
|
||||
}
|
||||
err = sess.Get(ctx, &max, q)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("unable to get RV %w", err)
|
||||
}
|
||||
if max.Valid && max.String != "" {
|
||||
t, _ := time.Parse(time.RFC3339, max.String)
|
||||
if !t.IsZero() {
|
||||
updated = t
|
||||
}
|
||||
} else {
|
||||
updated = s.startup
|
||||
}
|
||||
}
|
||||
|
||||
return stars, updated.UnixMilli(), err
|
||||
}
|
||||
|
||||
func (s *LegacySQL) getHistoryStars(ctx context.Context, orgId int64, user string) (map[string][]string, error) {
|
||||
sql, err := s.db(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := newStarQueryReq(sql, user, orgId)
|
||||
|
||||
q, err := sqltemplate.Execute(sqlHistoryStarsQuery, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute template %q: %w", sqlHistoryStarsQuery.Name(), err)
|
||||
}
|
||||
|
||||
sess := sql.DB.GetSqlxSession()
|
||||
rows, err := sess.Query(ctx, q, req.GetArgs()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if rows != nil {
|
||||
_ = rows.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
last := user
|
||||
res := make(map[string][]string)
|
||||
buffer := make([]string, 0, 10)
|
||||
var uid string
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&uid, &user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user != last && len(buffer) > 0 {
|
||||
res[last] = buffer
|
||||
buffer = make([]string, 0, 10)
|
||||
}
|
||||
buffer = append(buffer, uid)
|
||||
last = user
|
||||
}
|
||||
res[last] = buffer
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *LegacySQL) removeHistoryStar(ctx context.Context, user *user.User, stars []string) error {
|
||||
sql, err := s.db(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := newStarQueryReq(sql, "", user.OrgID)
|
||||
req.UserID = user.ID
|
||||
if len(stars) > 0 {
|
||||
req.QueryUIDs = stars
|
||||
}
|
||||
|
||||
q, err := sqltemplate.Execute(sqlHistoryStarsDelete, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("execute template %q: %w", sqlHistoryStarsDelete.Name(), err)
|
||||
}
|
||||
|
||||
sess := sql.DB.GetSqlxSession()
|
||||
_, err = sess.Exec(ctx, q, req.GetArgs()...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *LegacySQL) addHistoryStar(ctx context.Context, user *user.User, star string) error {
|
||||
sql, err := s.db(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := newStarQueryReq(sql, "", user.OrgID)
|
||||
req.UserID = user.ID
|
||||
req.QueryUID = star
|
||||
|
||||
q, err := sqltemplate.Execute(sqlHistoryStarsDelete, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("execute template %q: %w", sqlHistoryStarsDelete.Name(), err)
|
||||
}
|
||||
|
||||
sess := sql.DB.GetSqlxSession()
|
||||
_, err = sess.Exec(ctx, q, req.GetArgs()...)
|
||||
return err
|
||||
}
|
||||
|
||||
// List all defined preferences in an org (valid for admin users only)
|
||||
func (s *LegacySQL) listPreferences(ctx context.Context,
|
||||
ns string, orgId int64,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
SELECT s.query_uid, u.uid as user_uid
|
||||
FROM {{ .Ident .QueryHistoryStarsTable }} as s
|
||||
JOIN {{ .Ident .QueryHistoryTable }} as h ON s.query_uid = h.uid
|
||||
JOIN {{ .Ident .UserTable }} as u ON s.user_id = u.id
|
||||
WHERE s.org_id = {{ .Arg .OrgID }}
|
||||
{{ if .UserUID }}
|
||||
AND u.uid = {{ .Arg .UserUID }}
|
||||
{{ end }}
|
||||
ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc
|
||||
@@ -1,6 +0,0 @@
|
||||
DELETE FROM {{ .Ident .QueryHistoryStarsTable }}
|
||||
WHERE org_id = {{ .Arg .OrgID }}
|
||||
AND user_id = {{ .Arg .UserID }}
|
||||
{{ if .QueryUIDs }}
|
||||
AND query_uid IN ({{ .ArgList .QueryUIDs }})
|
||||
{{ end }}
|
||||
@@ -1,4 +0,0 @@
|
||||
INSERT INTO {{ .Ident .QueryHistoryStarsTable }}
|
||||
( query_uid, user_id, org_id )
|
||||
VALUES
|
||||
( {{ .Arg .QueryUID }}, {{ .Arg .UserID }}, {{ .Arg .OrgID }} )
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
SELECT s.query_uid, u.uid as user_uid
|
||||
FROM `grafana`.`query_history_star` as s
|
||||
JOIN `grafana`.`query_history` as h ON s.query_uid = h.uid
|
||||
JOIN `grafana`.`user` as u ON s.user_id = u.id
|
||||
WHERE s.org_id = 1
|
||||
ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
DELETE FROM `grafana`.`query_history_star`
|
||||
WHERE org_id = 1
|
||||
AND user_id = 3
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
INSERT INTO `grafana`.`query_history_star`
|
||||
( query_uid, user_id, org_id )
|
||||
VALUES
|
||||
( 'XXX', 3, 1 )
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
SELECT s.query_uid, u.uid as user_uid
|
||||
FROM "grafana"."query_history_star" as s
|
||||
JOIN "grafana"."query_history" as h ON s.query_uid = h.uid
|
||||
JOIN "grafana"."user" as u ON s.user_id = u.id
|
||||
WHERE s.org_id = 1
|
||||
ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc
|
||||
pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars_delete-remove all star.sql
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
DELETE FROM "grafana"."query_history_star"
|
||||
WHERE org_id = 1
|
||||
AND user_id = 3
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
INSERT INTO "grafana"."query_history_star"
|
||||
( query_uid, user_id, org_id )
|
||||
VALUES
|
||||
( 'XXX', 3, 1 )
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
SELECT s.query_uid, u.uid as user_uid
|
||||
FROM "grafana"."query_history_star" as s
|
||||
JOIN "grafana"."query_history" as h ON s.query_uid = h.uid
|
||||
JOIN "grafana"."user" as u ON s.user_id = u.id
|
||||
WHERE s.org_id = 1
|
||||
ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
DELETE FROM "grafana"."query_history_star"
|
||||
WHERE org_id = 1
|
||||
AND user_id = 3
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
INSERT INTO "grafana"."query_history_star"
|
||||
( query_uid, user_id, org_id )
|
||||
VALUES
|
||||
( 'XXX', 3, 1 )
|
||||
@@ -1,8 +1,6 @@
|
||||
package preferences
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -10,12 +8,9 @@ import (
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
|
||||
@@ -23,20 +18,17 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
"github.com/grafana/grafana/pkg/services/star"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
)
|
||||
|
||||
var (
|
||||
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
||||
_ builder.APIGroupMutation = (*APIBuilder)(nil)
|
||||
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
||||
)
|
||||
|
||||
type APIBuilder struct {
|
||||
authorizer authorizer.Authorizer
|
||||
legacyStars *legacy.DashboardStarsStorage
|
||||
legacyPrefs rest.Storage
|
||||
|
||||
merger *merger // joins all preferences
|
||||
@@ -47,7 +39,6 @@ func RegisterAPIService(
|
||||
features featuremgmt.FeatureToggles,
|
||||
db db.DB,
|
||||
prefs pref.Service,
|
||||
stars star.Service,
|
||||
users user.Service,
|
||||
apiregistration builder.APIRegistrar,
|
||||
) *APIBuilder {
|
||||
@@ -60,11 +51,10 @@ func RegisterAPIService(
|
||||
sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db))
|
||||
builder := &APIBuilder{
|
||||
merger: newMerger(cfg, sql),
|
||||
authorizer: &authorizeFromName{
|
||||
oknames: []string{"merged"},
|
||||
teams: sql, // should be from the IAM service
|
||||
resource: map[string][]utils.ResourceOwner{
|
||||
"stars": {utils.UserResourceOwner},
|
||||
authorizer: &utils.AuthorizeFromName{
|
||||
OKNames: []string{"merged"},
|
||||
Teams: sql, // should be from the IAM service
|
||||
Resource: map[string][]utils.ResourceOwner{
|
||||
"preferences": {
|
||||
utils.NamespaceResourceOwner,
|
||||
utils.TeamResourceOwner,
|
||||
@@ -78,10 +68,6 @@ func RegisterAPIService(
|
||||
if prefs != nil {
|
||||
builder.legacyPrefs = legacy.NewPreferencesStorage(prefs, namespacer, sql)
|
||||
}
|
||||
if stars != nil {
|
||||
builder.legacyStars = legacy.NewDashboardStarsStorage(stars, users, namespacer, sql)
|
||||
}
|
||||
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
@@ -109,24 +95,6 @@ func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
// Configure Stars Dual writer
|
||||
resource := preferences.StarsResourceInfo
|
||||
var stars grafanarest.Storage
|
||||
stars, err := grafanaregistry.NewRegistryStore(opts.Scheme, resource, opts.OptsGetter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stars = &starStorage{Storage: stars} // wrap List so we only return one value
|
||||
if b.legacyStars != nil && opts.DualWriteBuilder != nil {
|
||||
stars, err = opts.DualWriteBuilder(resource.GroupResource(), b.legacyStars, stars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
storage[resource.StoragePath()] = stars
|
||||
storage[resource.StoragePath("update")] = &starsREST{store: stars}
|
||||
|
||||
// Configure Preferences
|
||||
prefs := preferences.PreferencesResourceInfo
|
||||
storage[prefs.StoragePath()] = b.legacyPrefs
|
||||
|
||||
@@ -146,58 +114,3 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
|
||||
defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} })
|
||||
return b.merger.GetAPIRoutes(defs)
|
||||
}
|
||||
|
||||
func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
|
||||
oas.Info.Description = "Grafana preferences"
|
||||
|
||||
root := "/apis/" + b.GetGroupVersion().String() + "/"
|
||||
updateKey := root + "namespaces/{namespace}/stars/{name}/update"
|
||||
delete(oas.Paths.Paths, updateKey)
|
||||
|
||||
// Add the group/kind/id properties to the path
|
||||
stars, ok := oas.Paths.Paths[updateKey+"/{path}"]
|
||||
if !ok || stars == nil {
|
||||
return nil, fmt.Errorf("unable to find write path")
|
||||
}
|
||||
stars.Parameters = []*spec3.Parameter{
|
||||
stars.Parameters[0], // name
|
||||
stars.Parameters[1], // namespace
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "group",
|
||||
In: "path",
|
||||
Example: "dashboard.grafana.app",
|
||||
Description: "API group for stared item",
|
||||
Schema: spec.StringProperty(),
|
||||
Required: true,
|
||||
},
|
||||
}, {
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "kind",
|
||||
In: "path",
|
||||
Example: "Dashboard",
|
||||
Description: "Kind for stared item",
|
||||
Schema: spec.StringProperty(),
|
||||
Required: true,
|
||||
},
|
||||
}, {
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "id",
|
||||
In: "path",
|
||||
Example: "",
|
||||
Description: "The k8s name for the selected item",
|
||||
Schema: spec.StringProperty(),
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
stars.Put.Description = "Add a starred item"
|
||||
stars.Put.OperationId = "addStar"
|
||||
stars.Delete.Description = "Remove a starred item"
|
||||
stars.Delete.OperationId = "removeStar"
|
||||
|
||||
delete(oas.Paths.Paths, updateKey+"/{path}")
|
||||
oas.Paths.Paths[updateKey+"/{group}/{kind}/{id}"] = stars
|
||||
|
||||
return oas, nil
|
||||
}
|
||||
|
||||
+15
-16
@@ -1,4 +1,4 @@
|
||||
package preferences
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,16 +9,15 @@ import (
|
||||
"github.com/grafana/authlib/authz"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
|
||||
)
|
||||
|
||||
type authorizeFromName struct {
|
||||
teams utils.TeamService
|
||||
oknames []string
|
||||
resource map[string][]utils.ResourceOwner // may include unknown
|
||||
type AuthorizeFromName struct {
|
||||
Teams TeamService
|
||||
OKNames []string
|
||||
Resource map[string][]ResourceOwner // may include unknown
|
||||
}
|
||||
|
||||
func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
func (a *AuthorizeFromName) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil || user == nil {
|
||||
return authorizer.DecisionDeny, "valid user is required", err
|
||||
@@ -28,7 +27,7 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
|
||||
owners, ok := a.resource[attr.GetResource()]
|
||||
owners, ok := a.Resource[attr.GetResource()]
|
||||
if !ok {
|
||||
return authorizer.DecisionDeny, "missing resource name", nil
|
||||
}
|
||||
@@ -55,17 +54,17 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri
|
||||
}
|
||||
|
||||
// the pseudo sub-resource
|
||||
if a.oknames != nil && slices.Contains(a.oknames, attr.GetName()) {
|
||||
if a.OKNames != nil && slices.Contains(a.OKNames, attr.GetName()) {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
info, _ := utils.ParseOwnerFromName(attr.GetName())
|
||||
info, _ := ParseOwnerFromName(attr.GetName())
|
||||
if !slices.Contains(owners, info.Owner) {
|
||||
return authorizer.DecisionDeny, "unsupported owner type", nil
|
||||
}
|
||||
|
||||
switch info.Owner {
|
||||
case utils.NamespaceResourceOwner:
|
||||
case NamespaceResourceOwner:
|
||||
if attr.IsReadOnly() {
|
||||
// Everyone can see the namespace
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
@@ -75,17 +74,17 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri
|
||||
}
|
||||
return authorizer.DecisionDeny, "must be an org admin to edit", nil
|
||||
|
||||
case utils.UserResourceOwner:
|
||||
case UserResourceOwner:
|
||||
if user.GetIdentifier() == info.Identifier {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "your are not the owner of the resource", nil
|
||||
|
||||
case utils.TeamResourceOwner:
|
||||
if a.teams == nil {
|
||||
case TeamResourceOwner:
|
||||
if a.Teams == nil {
|
||||
return authorizer.DecisionDeny, "team checker not configured", err
|
||||
}
|
||||
ok, err := a.teams.InTeam(ctx, user, info.Identifier, !attr.IsReadOnly())
|
||||
ok, err := a.Teams.InTeam(ctx, user, info.Identifier, !attr.IsReadOnly())
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "error fetching teams", err
|
||||
}
|
||||
@@ -94,7 +93,7 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri
|
||||
}
|
||||
return authorizer.DecisionDeny, "you are not a member of the referenced team", nil
|
||||
|
||||
case utils.UnknownResourceOwner:
|
||||
case UnknownResourceOwner:
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
+19
-20
@@ -1,4 +1,4 @@
|
||||
package preferences
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
|
||||
)
|
||||
|
||||
type expect struct {
|
||||
@@ -41,14 +40,14 @@ func TestAuthorizer_Authorize(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
teams func(t *testing.T) utils.TeamService
|
||||
resource map[string][]utils.ResourceOwner
|
||||
teams func(t *testing.T) TeamService
|
||||
resource map[string][]ResourceOwner
|
||||
check []testCase
|
||||
}{
|
||||
{
|
||||
name: "stars",
|
||||
resource: map[string][]utils.ResourceOwner{
|
||||
"stars": {utils.UserResourceOwner},
|
||||
resource: map[string][]ResourceOwner{
|
||||
"stars": {UserResourceOwner},
|
||||
},
|
||||
check: []testCase{{
|
||||
name: "matches user",
|
||||
@@ -80,9 +79,9 @@ func TestAuthorizer_Authorize(t *testing.T) {
|
||||
}},
|
||||
}, {
|
||||
name: "fast path",
|
||||
resource: map[string][]utils.ResourceOwner{
|
||||
"stars": {utils.UserResourceOwner},
|
||||
"preferences": {utils.TeamResourceOwner},
|
||||
resource: map[string][]ResourceOwner{
|
||||
"stars": {UserResourceOwner},
|
||||
"preferences": {TeamResourceOwner},
|
||||
},
|
||||
check: []testCase{{
|
||||
name: "missing user",
|
||||
@@ -185,8 +184,8 @@ func TestAuthorizer_Authorize(t *testing.T) {
|
||||
}},
|
||||
}, {
|
||||
name: "unknown owner",
|
||||
resource: map[string][]utils.ResourceOwner{
|
||||
"stars": {utils.UnknownResourceOwner},
|
||||
resource: map[string][]ResourceOwner{
|
||||
"stars": {UnknownResourceOwner},
|
||||
},
|
||||
check: []testCase{{
|
||||
name: "get",
|
||||
@@ -204,8 +203,8 @@ func TestAuthorizer_Authorize(t *testing.T) {
|
||||
}},
|
||||
}, {
|
||||
name: "namespace",
|
||||
resource: map[string][]utils.ResourceOwner{
|
||||
"ns": {utils.NamespaceResourceOwner},
|
||||
resource: map[string][]ResourceOwner{
|
||||
"ns": {NamespaceResourceOwner},
|
||||
},
|
||||
check: []testCase{{
|
||||
name: "readonly",
|
||||
@@ -258,16 +257,16 @@ func TestAuthorizer_Authorize(t *testing.T) {
|
||||
}},
|
||||
}, {
|
||||
name: "preferences teams",
|
||||
teams: func(t *testing.T) utils.TeamService {
|
||||
teams := utils.NewMockTeamService(t)
|
||||
teams: func(t *testing.T) TeamService {
|
||||
teams := NewMockTeamService(t)
|
||||
teams.On("InTeam", mock.Anything, userABC, "xyz", false).Return(true, nil)
|
||||
teams.On("InTeam", mock.Anything, userABC, "456", false).Return(false, nil)
|
||||
teams.On("InTeam", mock.Anything, userABC, "XXX", false).Return(true, fmt.Errorf("error from team"))
|
||||
return teams
|
||||
},
|
||||
resource: map[string][]utils.ResourceOwner{
|
||||
resource: map[string][]ResourceOwner{
|
||||
"preferences": {
|
||||
utils.TeamResourceOwner,
|
||||
TeamResourceOwner,
|
||||
},
|
||||
},
|
||||
check: []testCase{{
|
||||
@@ -318,11 +317,11 @@ func TestAuthorizer_Authorize(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authz := &authorizeFromName{
|
||||
resource: tt.resource,
|
||||
authz := &AuthorizeFromName{
|
||||
Resource: tt.resource,
|
||||
}
|
||||
if tt.teams != nil {
|
||||
authz.teams = tt.teams(t)
|
||||
authz.Teams = tt.teams(t)
|
||||
}
|
||||
for _, check := range tt.check {
|
||||
t.Run(check.name, func(t *testing.T) {
|
||||
@@ -3,6 +3,7 @@ package apiregistry
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections"
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
@@ -63,6 +64,7 @@ var WireSet = wire.NewSet(
|
||||
service.RegisterAPIService,
|
||||
query.RegisterAPIService,
|
||||
preferences.RegisterAPIService,
|
||||
collections.RegisterAPIService,
|
||||
userstorage.RegisterAPIService,
|
||||
ofrep.RegisterAPIService,
|
||||
)
|
||||
|
||||
Generated
+7
-4
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
"github.com/grafana/grafana/pkg/registry/apis"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
@@ -866,7 +867,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
return nil, err
|
||||
}
|
||||
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
|
||||
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService)
|
||||
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, userService, apiserverService)
|
||||
collectionsAPIBuilder := collections.RegisterAPIService(cfg, featureToggles, sqlStore, starService, userService, apiserverService)
|
||||
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles)
|
||||
webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer)
|
||||
v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
|
||||
@@ -900,7 +902,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1507,7 +1509,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
return nil, err
|
||||
}
|
||||
userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer)
|
||||
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService)
|
||||
apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, userService, apiserverService)
|
||||
collectionsAPIBuilder := collections.RegisterAPIService(cfg, featureToggles, sqlStore, starService, userService, apiserverService)
|
||||
legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles)
|
||||
webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer)
|
||||
v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
|
||||
@@ -1541,7 +1544,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer)
|
||||
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -168,16 +168,6 @@ func (s *QueryHistoryService) starHandler(c *contextmodel.ReqContext) response.R
|
||||
if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) {
|
||||
return response.Error(http.StatusNotFound, "Query in query history not found", nil)
|
||||
}
|
||||
if s.k8sClients != nil {
|
||||
if err := s.k8sClients.AddStar(c, queryUID); err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to star query in query history", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, QueryHistoryResponse{
|
||||
Result: QueryHistoryDTO{
|
||||
UID: queryUID,
|
||||
Starred: true,
|
||||
}})
|
||||
}
|
||||
|
||||
query, err := s.StarQueryInQueryHistory(c.Req.Context(), c.SignedInUser, queryUID)
|
||||
if err != nil {
|
||||
@@ -202,17 +192,6 @@ func (s *QueryHistoryService) unstarHandler(c *contextmodel.ReqContext) response
|
||||
if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) {
|
||||
return response.Error(http.StatusNotFound, "Query in query history not found", nil)
|
||||
}
|
||||
if s.k8sClients != nil {
|
||||
if err := s.k8sClients.RemoveStar(c, queryUID); err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to star query in query history", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, QueryHistoryResponse{
|
||||
Result: QueryHistoryDTO{
|
||||
UID: queryUID,
|
||||
Starred: true,
|
||||
}})
|
||||
}
|
||||
|
||||
query, err := s.UnstarQueryInQueryHistory(c.Req.Context(), c.SignedInUser, queryUID)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to unstar query in query history", err)
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package queryhistory
|
||||
|
||||
import (
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
preferencesV1 "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
)
|
||||
|
||||
type k8sClients struct {
|
||||
namespacer authlib.NamespaceFormatter
|
||||
configProvider apiserver.DirectRestConfigProvider
|
||||
}
|
||||
|
||||
// GetStars implements K8sClients.
|
||||
func (k *k8sClients) GetStars(c *contextmodel.ReqContext) ([]string, error) {
|
||||
dyn, err := dynamic.NewForConfig(k.configProvider.GetDirectRestConfig(c))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := dyn.Resource(preferencesV1.StarsResourceInfo.GroupVersionResource()).Namespace(k.namespacer(c.OrgID))
|
||||
|
||||
ctx := c.Req.Context()
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj, _ := client.Get(ctx, "user-"+user.GetIdentifier(), v1.GetOptions{})
|
||||
if obj != nil {
|
||||
resources, ok, _ := unstructured.NestedSlice(obj.Object, "spec", "resource")
|
||||
if ok && resources != nil {
|
||||
for _, r := range resources {
|
||||
tmp, ok := r.(map[string]any)
|
||||
if ok {
|
||||
g, _, _ := unstructured.NestedString(tmp, "group")
|
||||
k, _, _ := unstructured.NestedString(tmp, "kind")
|
||||
if k == "Query" && g == "history.grafana.app" {
|
||||
names, _, _ := unstructured.NestedStringSlice(tmp, "names")
|
||||
return names, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// AddStar implements K8sClients.
|
||||
func (k *k8sClients) AddStar(c *contextmodel.ReqContext, uid string) error {
|
||||
dyn, err := kubernetes.NewForConfig(k.configProvider.GetDirectRestConfig(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Req.Context()
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns := k.namespacer(c.OrgID)
|
||||
|
||||
client := dyn.RESTClient()
|
||||
rsp := client.Put().AbsPath(
|
||||
"apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns,
|
||||
"stars", "user-"+user.GetIdentifier(),
|
||||
"update", "history.grafana.app", "Query", uid,
|
||||
).Do(ctx)
|
||||
|
||||
return rsp.Error()
|
||||
}
|
||||
|
||||
// RemoveStar implements K8sClients.
|
||||
func (k *k8sClients) RemoveStar(c *contextmodel.ReqContext, uid string) error {
|
||||
dyn, err := kubernetes.NewForConfig(k.configProvider.GetDirectRestConfig(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Req.Context()
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ns := k.namespacer(c.OrgID)
|
||||
|
||||
client := dyn.RESTClient()
|
||||
rsp := client.Delete().AbsPath(
|
||||
"apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns,
|
||||
"stars", "user-"+user.GetIdentifier(),
|
||||
"update", "history.grafana.app", "Query", uid,
|
||||
).Do(ctx)
|
||||
|
||||
return rsp.Error()
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
ac "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/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -33,13 +32,6 @@ func ProvideService(cfg *setting.Cfg,
|
||||
|
||||
// Register routes only when query history is enabled
|
||||
if s.Cfg.QueryHistoryEnabled {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesStars) {
|
||||
s.k8sClients = &k8sClients{
|
||||
namespacer: request.GetNamespaceMapper(s.Cfg),
|
||||
configProvider: configProvider,
|
||||
}
|
||||
}
|
||||
s.registerAPIEndpoints()
|
||||
}
|
||||
|
||||
@@ -64,7 +56,6 @@ type QueryHistoryService struct {
|
||||
log log.Logger
|
||||
now func() time.Time
|
||||
accessControl ac.AccessControl
|
||||
k8sClients *k8sClients
|
||||
}
|
||||
|
||||
func (s QueryHistoryService) CreateQueryInQueryHistory(ctx context.Context, user *user.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error) {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
collectionsV1 "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
dashboardsV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
preferencesV1 "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
@@ -59,7 +59,7 @@ func (k *k8sClients) GetStars(c *contextmodel.ReqContext) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := dyn.Resource(preferencesV1.StarsResourceInfo.GroupVersionResource()).Namespace(k.namespacer(c.OrgID))
|
||||
client := dyn.Resource(collectionsV1.StarsResourceInfo.GroupVersionResource()).Namespace(k.namespacer(c.OrgID))
|
||||
|
||||
ctx := c.Req.Context()
|
||||
user, err := identity.GetRequester(ctx)
|
||||
@@ -104,7 +104,7 @@ func (k *k8sClients) AddStar(c *contextmodel.ReqContext, uid string) error {
|
||||
|
||||
client := dyn.RESTClient()
|
||||
rsp := client.Put().AbsPath(
|
||||
"apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns,
|
||||
"apis", collectionsV1.APIGroup, collectionsV1.APIVersion, "namespaces", ns,
|
||||
"stars", "user-"+user.GetIdentifier(),
|
||||
"update", dashboardsV1.APIGroup, dashboardsV1.DashboardKind().Kind(), uid,
|
||||
).Do(ctx)
|
||||
@@ -129,7 +129,7 @@ func (k *k8sClients) RemoveStar(c *contextmodel.ReqContext, uid string) error {
|
||||
|
||||
client := dyn.RESTClient()
|
||||
rsp := client.Delete().AbsPath(
|
||||
"apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns,
|
||||
"apis", collectionsV1.APIGroup, collectionsV1.APIVersion, "namespaces", ns,
|
||||
"stars", "user-"+user.GetIdentifier(),
|
||||
"update", dashboardsV1.APIGroup, dashboardsV1.DashboardKind().Kind(), uid,
|
||||
).Do(ctx)
|
||||
|
||||
+15
-38
@@ -1,4 +1,4 @@
|
||||
package preferences
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -11,17 +11,21 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/queryhistory"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestIntegrationStars(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
@@ -47,10 +51,10 @@ func TestIntegrationStars(t *testing.T) {
|
||||
"folders.folder.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
},
|
||||
"stars.preferences.grafana.app": {
|
||||
"stars.collections.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
},
|
||||
"preferences.preferences.grafana.app": {
|
||||
"collections.collections.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
},
|
||||
},
|
||||
@@ -60,28 +64,17 @@ func TestIntegrationStars(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
starsClient := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Admin,
|
||||
GVR: preferences.StarsResourceInfo.GroupVersionResource(),
|
||||
GVR: collections.StarsResourceInfo.GroupVersionResource(),
|
||||
})
|
||||
starsClientViewer := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Viewer,
|
||||
GVR: preferences.StarsResourceInfo.GroupVersionResource(),
|
||||
GVR: collections.StarsResourceInfo.GroupVersionResource(),
|
||||
})
|
||||
dashboardClient := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Admin,
|
||||
GVR: dashboardV1.DashboardResourceInfo.GroupVersionResource(),
|
||||
})
|
||||
|
||||
history := &queryhistory.QueryHistoryResponse{}
|
||||
legacyHistoryResponse := apis.DoRequest(helper, apis.RequestParams{
|
||||
User: starsClient.Args.User,
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/query-history",
|
||||
Body: []byte(`{"dataSourceUid":"eez1ebbdn3pq8b","queries":[{"scenarioId":"random_walk","seriesCount":1,"refId":"A","datasource":{"type":"grafana-testdata-datasource","uid":"eez1ebbdn3pq8b","apiVersion":"v0alpha1"}}]}`),
|
||||
}, &history)
|
||||
require.Equal(t, http.StatusOK, legacyHistoryResponse.Response.StatusCode, "add query history")
|
||||
queryHistoryStarUID := history.Result.UID
|
||||
require.NotEmpty(t, queryHistoryStarUID, "expect a query history UID")
|
||||
|
||||
// Create 5 dashboards
|
||||
for i := range 5 {
|
||||
_, err := dashboardClient.Resource.Create(context.Background(), &unstructured.Unstructured{
|
||||
@@ -126,7 +119,7 @@ func TestIntegrationStars(t *testing.T) {
|
||||
// List values and compare results
|
||||
rsp, err = starsClient.Resource.List(ctx, metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
stars := typed(t, rsp, &preferences.StarsList{})
|
||||
stars := typed(t, rsp, &collections.StarsList{})
|
||||
|
||||
require.Len(t, stars.Items, 1, "user stars should exist")
|
||||
require.Equal(t, "user-"+starsClient.Args.User.Identity.GetIdentifier(),
|
||||
@@ -148,7 +141,7 @@ func TestIntegrationStars(t *testing.T) {
|
||||
rspObj, err := starsClient.Resource.Get(ctx, "user-"+starsClient.Args.User.Identity.GetIdentifier(), metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
after := typed(t, rspObj, &preferences.Stars{})
|
||||
after := typed(t, rspObj, &collections.Stars{})
|
||||
resources = after.Spec.Resource
|
||||
require.Len(t, resources, 1)
|
||||
require.Equal(t, "dashboard.grafana.app", resources[0].Group)
|
||||
@@ -175,7 +168,7 @@ func TestIntegrationStars(t *testing.T) {
|
||||
}, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
after = typed(t, rspObj, &preferences.Stars{})
|
||||
after = typed(t, rspObj, &collections.Stars{})
|
||||
resources = after.Spec.Resource
|
||||
require.Len(t, resources, 1)
|
||||
require.Equal(t, "dashboard.grafana.app", resources[0].Group)
|
||||
@@ -184,19 +177,10 @@ func TestIntegrationStars(t *testing.T) {
|
||||
[]string{"aaa", "bbb", "test-2"}, // NOTE 2 stays, 3 removed, added aaa+bbb (and sorted!)
|
||||
resources[0].Names)
|
||||
|
||||
// Query history stars
|
||||
legacyHistoryResponse = apis.DoRequest(helper, apis.RequestParams{
|
||||
User: starsClient.Args.User,
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/query-history/star/" + queryHistoryStarUID,
|
||||
}, &history)
|
||||
require.Equal(t, http.StatusOK, legacyHistoryResponse.Response.StatusCode, "add query history")
|
||||
require.True(t, history.Result.Starred, "expect the value to be starred")
|
||||
|
||||
rspObj, err = starsClient.Resource.Get(ctx, "user-"+starsClient.Args.User.Identity.GetIdentifier(), metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
after = typed(t, rspObj, &preferences.Stars{})
|
||||
after = typed(t, rspObj, &collections.Stars{})
|
||||
jj, err := json.MarshalIndent(after.Spec, "", " ")
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{
|
||||
@@ -209,13 +193,6 @@ func TestIntegrationStars(t *testing.T) {
|
||||
"bbb",
|
||||
"test-2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "history.grafana.app",
|
||||
"kind": "Query",
|
||||
"names": [
|
||||
"`+queryHistoryStarUID+`"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, string(jj))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,9 @@ func TestIntegrationOpenAPIs(t *testing.T) {
|
||||
}, {
|
||||
Group: "preferences.grafana.app",
|
||||
Version: "v1alpha1",
|
||||
}, {
|
||||
Group: "collections.grafana.app",
|
||||
Version: "v1alpha1",
|
||||
}, {
|
||||
Group: "notifications.alerting.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
|
||||
Reference in New Issue
Block a user