Stars: support running stars in mode 5 (#111754)
This commit is contained in:
@@ -115,6 +115,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
starsFromAPIServer?: boolean;
|
||||
/**
|
||||
* Routes stars requests from /api to the /apis endpoint
|
||||
*/
|
||||
kubernetesStars?: boolean;
|
||||
/**
|
||||
* Enable streaming JSON parser for InfluxDB datasource InfluxQL query language
|
||||
*/
|
||||
influxqlStreamingParser?: boolean;
|
||||
|
||||
@@ -112,6 +112,7 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stars = &starStorage{store: 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 {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package preferences
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"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"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
)
|
||||
|
||||
var _ grafanarest.Storage = (*starStorage)(nil)
|
||||
|
||||
type starStorage struct {
|
||||
store grafanarest.Storage
|
||||
}
|
||||
|
||||
// When using list, we really just want to get the value for the single user
|
||||
func (s *starStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch user.GetIdentityType() {
|
||||
case authlib.TypeAnonymous:
|
||||
return s.NewList(), nil
|
||||
|
||||
// Get the single user stars
|
||||
case authlib.TypeUser:
|
||||
stars := &preferences.StarsList{}
|
||||
obj, _ := s.store.Get(ctx, "user-"+user.GetIdentifier(), &v1.GetOptions{})
|
||||
if obj != nil {
|
||||
s, ok := obj.(*preferences.Stars)
|
||||
if ok {
|
||||
stars.Items = []preferences.Stars{*s}
|
||||
}
|
||||
}
|
||||
return stars, nil
|
||||
|
||||
default:
|
||||
return s.store.List(ctx, options)
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToTable implements rest.Storage.
|
||||
func (s *starStorage) ConvertToTable(ctx context.Context, obj runtime.Object, tableOptions runtime.Object) (*v1.Table, error) {
|
||||
return s.store.ConvertToTable(ctx, obj, tableOptions)
|
||||
}
|
||||
|
||||
// Create implements rest.Storage.
|
||||
func (s *starStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *v1.CreateOptions) (runtime.Object, error) {
|
||||
return s.store.Create(ctx, obj, createValidation, options)
|
||||
}
|
||||
|
||||
// Delete implements rest.Storage.
|
||||
func (s *starStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *v1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
return s.store.Delete(ctx, name, deleteValidation, options)
|
||||
}
|
||||
|
||||
// DeleteCollection implements rest.Storage.
|
||||
func (s *starStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *v1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return s.store.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||
}
|
||||
|
||||
// Destroy implements rest.Storage.
|
||||
func (s *starStorage) Destroy() {
|
||||
s.store.Destroy()
|
||||
}
|
||||
|
||||
// Get implements rest.Storage.
|
||||
func (s *starStorage) Get(ctx context.Context, name string, options *v1.GetOptions) (runtime.Object, error) {
|
||||
return s.store.Get(ctx, name, options)
|
||||
}
|
||||
|
||||
// GetSingularName implements rest.Storage.
|
||||
func (s *starStorage) GetSingularName() string {
|
||||
return s.store.GetSingularName()
|
||||
}
|
||||
|
||||
// NamespaceScoped implements rest.Storage.
|
||||
func (s *starStorage) NamespaceScoped() bool {
|
||||
return s.store.NamespaceScoped()
|
||||
}
|
||||
|
||||
// New implements rest.Storage.
|
||||
func (s *starStorage) New() runtime.Object {
|
||||
return s.store.New()
|
||||
}
|
||||
|
||||
// NewList implements rest.Storage.
|
||||
func (s *starStorage) NewList() runtime.Object {
|
||||
return s.store.NewList()
|
||||
}
|
||||
|
||||
// Update implements rest.Storage.
|
||||
func (s *starStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *v1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
}
|
||||
@@ -705,7 +705,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
searchHTTPService := searchV2.ProvideSearchHTTPService(searchService)
|
||||
statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, resourceClient, featureToggles)
|
||||
gatherer := metrics.ProvideGatherer()
|
||||
apiAPI := api3.ProvideApi(starService, dashboardService)
|
||||
apiAPI := api3.ProvideApi(cfg, featureToggles, starService, eventualRestConfigProvider)
|
||||
anonUserLimitValidatorImpl := validator2.ProvideAnonUserLimitValidator()
|
||||
anonDeviceService := anonimpl.ProvideAnonymousDeviceService(usageStats, authnService, sqlStore, cfg, orgService, serverLockService, accessControl, routeRegisterImpl, anonUserLimitValidatorImpl)
|
||||
signingkeysimplService, err := signingkeysimpl.ProvideEmbeddedSigningKeysService(sqlStore, secretsService, remoteCache, routeRegisterImpl)
|
||||
@@ -1312,7 +1312,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
searchHTTPService := searchV2.ProvideSearchHTTPService(searchService)
|
||||
statsService := statsimpl.ProvideService(cfg, sqlStore, dashboardService, folderimplService, orgService, resourceClient, featureToggles)
|
||||
gatherer := metrics.ProvideGathererForTest(registerer)
|
||||
apiAPI := api3.ProvideApi(starService, dashboardService)
|
||||
apiAPI := api3.ProvideApi(cfg, featureToggles, starService, eventualRestConfigProvider)
|
||||
anonUserLimitValidatorImpl := validator2.ProvideAnonUserLimitValidator()
|
||||
anonDeviceService := anonimpl.ProvideAnonymousDeviceService(usageStats, authnService, sqlStore, cfg, orgService, serverLockService, accessControl, routeRegisterImpl, anonUserLimitValidatorImpl)
|
||||
signingkeysimplService, err := signingkeysimpl.ProvideEmbeddedSigningKeysService(sqlStore, secretsService, remoteCache, routeRegisterImpl)
|
||||
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
)
|
||||
|
||||
type RestConfigProvider interface {
|
||||
|
||||
@@ -180,6 +180,13 @@ var (
|
||||
AllowSelfServe: false,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesStars",
|
||||
Description: "Routes stars requests from /api to the /apis endpoint",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true, // changes the API routing
|
||||
},
|
||||
{
|
||||
Name: "influxqlStreamingParser",
|
||||
Description: "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language",
|
||||
|
||||
@@ -21,6 +21,7 @@ lokiQuerySplitting,GA,@grafana/observability-logs,false,false,true
|
||||
individualCookiePreferences,experimental,@grafana/grafana-backend-group,false,false,false
|
||||
influxdbBackendMigration,GA,@grafana/partner-datasources,false,false,true
|
||||
starsFromAPIServer,experimental,@grafana/grafana-frontend-platform,false,false,true
|
||||
kubernetesStars,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false
|
||||
influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false
|
||||
|
||||
|
@@ -95,6 +95,10 @@ const (
|
||||
// populate star status from apiserver
|
||||
FlagStarsFromAPIServer = "starsFromAPIServer"
|
||||
|
||||
// FlagKubernetesStars
|
||||
// Routes stars requests from /api to the /apis endpoint
|
||||
FlagKubernetesStars = "kubernetesStars"
|
||||
|
||||
// FlagInfluxqlStreamingParser
|
||||
// Enable streaming JSON parser for InfluxDB datasource InfluxQL query language
|
||||
FlagInfluxqlStreamingParser = "influxqlStreamingParser"
|
||||
|
||||
@@ -2188,6 +2188,19 @@
|
||||
"requiresRestart": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesStars",
|
||||
"resourceVersion": "1759149842036",
|
||||
"creationTimestamp": "2025-09-29T12:44:02Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Routes stars requests from /api to the /apis endpoint",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-app-platform-squad",
|
||||
"requiresRestart": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "localeFormatPreference",
|
||||
|
||||
@@ -1,56 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/star"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
starService star.Service
|
||||
dashboardService dashboards.DashboardService
|
||||
logger log.Logger
|
||||
starService star.Service
|
||||
client K8sClients
|
||||
}
|
||||
|
||||
func ProvideApi(
|
||||
cfg *setting.Cfg, // for namespacer
|
||||
features featuremgmt.FeatureToggles,
|
||||
starService star.Service,
|
||||
dashboardService dashboards.DashboardService,
|
||||
configProvider apiserver.DirectRestConfigProvider,
|
||||
) *API {
|
||||
starLogger := log.New("stars.api")
|
||||
api := &API{
|
||||
starService: starService,
|
||||
dashboardService: dashboardService,
|
||||
logger: starLogger,
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesStars) {
|
||||
starService = nil // don't use it
|
||||
}
|
||||
return api
|
||||
}
|
||||
|
||||
func (api *API) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*dashboards.Dashboard, response.Response) {
|
||||
var query dashboards.GetDashboardQuery
|
||||
|
||||
if len(uid) > 0 {
|
||||
query = dashboards.GetDashboardQuery{UID: uid, ID: id, OrgID: orgID}
|
||||
} else {
|
||||
query = dashboards.GetDashboardQuery{ID: id, OrgID: orgID}
|
||||
return &API{
|
||||
starService: starService,
|
||||
client: &k8sClients{
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
configProvider: configProvider,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := api.dashboardService.GetDashboard(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, response.Error(http.StatusNotFound, "Dashboard not found", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (api *API) GetStars(c *contextmodel.ReqContext) response.Response {
|
||||
if api.starService == nil {
|
||||
stars, err := api.client.GetStars(c)
|
||||
if err != nil {
|
||||
logging.FromContext(c.Req.Context()).With("logger", "star.api").Warn("error", "err", err)
|
||||
}
|
||||
return response.JSON(http.StatusOK, stars)
|
||||
}
|
||||
|
||||
query := star.GetUserStarsQuery{
|
||||
UserID: c.UserID,
|
||||
}
|
||||
@@ -86,17 +83,25 @@ func (api *API) StarDashboardByUID(c *contextmodel.ReqContext) response.Response
|
||||
return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil)
|
||||
}
|
||||
|
||||
if api.starService == nil {
|
||||
err := api.client.AddStar(c, uid)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to star dashboard", err)
|
||||
}
|
||||
return response.Success("Dashboard starred!")
|
||||
}
|
||||
|
||||
userID, err := identity.UserIdentifier(c.GetID())
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Only users and service accounts can star dashboards", nil)
|
||||
}
|
||||
|
||||
dash, rsp := api.getDashboardHelper(c.Req.Context(), c.GetOrgID(), 0, uid)
|
||||
dashboardID, rsp := api.client.GetDashboardID(c, uid)
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
cmd := star.StarDashboardCommand{UserID: userID, DashboardID: dash.ID, DashboardUID: uid, OrgID: c.GetOrgID(), Updated: time.Now()}
|
||||
cmd := star.StarDashboardCommand{UserID: userID, DashboardID: dashboardID, DashboardUID: uid, OrgID: c.GetOrgID(), Updated: time.Now()}
|
||||
|
||||
if err := api.starService.Add(c.Req.Context(), &cmd); err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to star dashboard", err)
|
||||
@@ -123,6 +128,14 @@ func (api *API) UnstarDashboardByUID(c *contextmodel.ReqContext) response.Respon
|
||||
return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil)
|
||||
}
|
||||
|
||||
if api.starService == nil {
|
||||
err := api.client.RemoveStar(c, uid)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to unstar dashboard", err)
|
||||
}
|
||||
return response.Success("Dashboard unstarred")
|
||||
}
|
||||
|
||||
userID, err := identity.UserIdentifier(c.GetID())
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Only users and service accounts can star dashboards", nil)
|
||||
|
||||
@@ -8,16 +8,18 @@ import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/star/startest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func TestStarDashboardUID(t *testing.T) {
|
||||
svc := dashboards.NewFakeDashboardService(t)
|
||||
svc.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{UID: "test", OrgID: 1}, nil)
|
||||
api := ProvideApi(startest.NewStarServiceFake(), svc)
|
||||
client := NewMockK8sClients(t)
|
||||
client.On("GetDashboardID", mock.Anything, mock.Anything).Return(int64(123), nil)
|
||||
api := &API{
|
||||
starService: startest.NewStarServiceFake(),
|
||||
client: client,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
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"
|
||||
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"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
)
|
||||
|
||||
//go:generate mockery --name K8sClients --structname MockK8sClients --inpackage --filename client_mock.go --with-expecter
|
||||
type K8sClients interface {
|
||||
GetDashboardID(c *contextmodel.ReqContext, uid string) (int64, response.Response)
|
||||
GetStars(c *contextmodel.ReqContext) ([]string, error)
|
||||
AddStar(c *contextmodel.ReqContext, uid string) error
|
||||
RemoveStar(c *contextmodel.ReqContext, uid string) error
|
||||
}
|
||||
|
||||
type k8sClients struct {
|
||||
namespacer authlib.NamespaceFormatter
|
||||
configProvider apiserver.DirectRestConfigProvider
|
||||
}
|
||||
|
||||
var (
|
||||
_ K8sClients = (*k8sClients)(nil)
|
||||
)
|
||||
|
||||
// GetDashboardID implements the K8sClients interface.
|
||||
func (k *k8sClients) GetDashboardID(c *contextmodel.ReqContext, uid string) (int64, response.Response) {
|
||||
dyn, err := dynamic.NewForConfig(k.configProvider.GetDirectRestConfig(c))
|
||||
if err != nil {
|
||||
return 0, response.Error(http.StatusInternalServerError, "client config", err)
|
||||
}
|
||||
client := dyn.Resource(dashboardsV1.GroupVersion.WithResource(dashboardsV1.DASHBOARD_RESOURCE)).Namespace(k.namespacer(c.OrgID))
|
||||
obj, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return 0, response.Error(http.StatusNotFound, "Dashboard not found", err)
|
||||
}
|
||||
dash, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
return 0, response.Error(http.StatusInternalServerError, "invalid object", err)
|
||||
}
|
||||
return dash.GetDeprecatedInternalID(), nil // nolint:staticcheck
|
||||
}
|
||||
|
||||
// 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 == "Dashboard" && g == dashboardsV1.APIGroup {
|
||||
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", dashboardsV1.APIGroup, dashboardsV1.DashboardKind().Kind(), 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", dashboardsV1.APIGroup, dashboardsV1.DashboardKind().Kind(), uid,
|
||||
).Do(ctx)
|
||||
|
||||
return rsp.Error()
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
response "github.com/grafana/grafana/pkg/api/response"
|
||||
)
|
||||
|
||||
// MockK8sClients is an autogenerated mock type for the K8sClients type
|
||||
type MockK8sClients struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockK8sClients_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockK8sClients) EXPECT() *MockK8sClients_Expecter {
|
||||
return &MockK8sClients_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// AddStar provides a mock function with given fields: c, uid
|
||||
func (_m *MockK8sClients) AddStar(c *contextmodel.ReqContext, uid string) error {
|
||||
ret := _m.Called(c, uid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AddStar")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*contextmodel.ReqContext, string) error); ok {
|
||||
r0 = rf(c, uid)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockK8sClients_AddStar_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddStar'
|
||||
type MockK8sClients_AddStar_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AddStar is a helper method to define mock.On call
|
||||
// - c *contextmodel.ReqContext
|
||||
// - uid string
|
||||
func (_e *MockK8sClients_Expecter) AddStar(c interface{}, uid interface{}) *MockK8sClients_AddStar_Call {
|
||||
return &MockK8sClients_AddStar_Call{Call: _e.mock.On("AddStar", c, uid)}
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_AddStar_Call) Run(run func(c *contextmodel.ReqContext, uid string)) *MockK8sClients_AddStar_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*contextmodel.ReqContext), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_AddStar_Call) Return(_a0 error) *MockK8sClients_AddStar_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_AddStar_Call) RunAndReturn(run func(*contextmodel.ReqContext, string) error) *MockK8sClients_AddStar_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetDashboardID provides a mock function with given fields: c, uid
|
||||
func (_m *MockK8sClients) GetDashboardID(c *contextmodel.ReqContext, uid string) (int64, response.Response) {
|
||||
ret := _m.Called(c, uid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetDashboardID")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 response.Response
|
||||
if rf, ok := ret.Get(0).(func(*contextmodel.ReqContext, string) (int64, response.Response)); ok {
|
||||
return rf(c, uid)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*contextmodel.ReqContext, string) int64); ok {
|
||||
r0 = rf(c, uid)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*contextmodel.ReqContext, string) response.Response); ok {
|
||||
r1 = rf(c, uid)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(response.Response)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockK8sClients_GetDashboardID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDashboardID'
|
||||
type MockK8sClients_GetDashboardID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetDashboardID is a helper method to define mock.On call
|
||||
// - c *contextmodel.ReqContext
|
||||
// - uid string
|
||||
func (_e *MockK8sClients_Expecter) GetDashboardID(c interface{}, uid interface{}) *MockK8sClients_GetDashboardID_Call {
|
||||
return &MockK8sClients_GetDashboardID_Call{Call: _e.mock.On("GetDashboardID", c, uid)}
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_GetDashboardID_Call) Run(run func(c *contextmodel.ReqContext, uid string)) *MockK8sClients_GetDashboardID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*contextmodel.ReqContext), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_GetDashboardID_Call) Return(_a0 int64, _a1 response.Response) *MockK8sClients_GetDashboardID_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_GetDashboardID_Call) RunAndReturn(run func(*contextmodel.ReqContext, string) (int64, response.Response)) *MockK8sClients_GetDashboardID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetStars provides a mock function with given fields: c
|
||||
func (_m *MockK8sClients) GetStars(c *contextmodel.ReqContext) ([]string, error) {
|
||||
ret := _m.Called(c)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetStars")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*contextmodel.ReqContext) ([]string, error)); ok {
|
||||
return rf(c)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*contextmodel.ReqContext) []string); ok {
|
||||
r0 = rf(c)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*contextmodel.ReqContext) error); ok {
|
||||
r1 = rf(c)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockK8sClients_GetStars_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStars'
|
||||
type MockK8sClients_GetStars_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetStars is a helper method to define mock.On call
|
||||
// - c *contextmodel.ReqContext
|
||||
func (_e *MockK8sClients_Expecter) GetStars(c interface{}) *MockK8sClients_GetStars_Call {
|
||||
return &MockK8sClients_GetStars_Call{Call: _e.mock.On("GetStars", c)}
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_GetStars_Call) Run(run func(c *contextmodel.ReqContext)) *MockK8sClients_GetStars_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*contextmodel.ReqContext))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_GetStars_Call) Return(_a0 []string, _a1 error) *MockK8sClients_GetStars_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_GetStars_Call) RunAndReturn(run func(*contextmodel.ReqContext) ([]string, error)) *MockK8sClients_GetStars_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveStar provides a mock function with given fields: c, uid
|
||||
func (_m *MockK8sClients) RemoveStar(c *contextmodel.ReqContext, uid string) error {
|
||||
ret := _m.Called(c, uid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveStar")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*contextmodel.ReqContext, string) error); ok {
|
||||
r0 = rf(c, uid)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockK8sClients_RemoveStar_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveStar'
|
||||
type MockK8sClients_RemoveStar_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoveStar is a helper method to define mock.On call
|
||||
// - c *contextmodel.ReqContext
|
||||
// - uid string
|
||||
func (_e *MockK8sClients_Expecter) RemoveStar(c interface{}, uid interface{}) *MockK8sClients_RemoveStar_Call {
|
||||
return &MockK8sClients_RemoveStar_Call{Call: _e.mock.On("RemoveStar", c, uid)}
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_RemoveStar_Call) Run(run func(c *contextmodel.ReqContext, uid string)) *MockK8sClients_RemoveStar_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*contextmodel.ReqContext), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_RemoveStar_Call) Return(_a0 error) *MockK8sClients_RemoveStar_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockK8sClients_RemoveStar_Call) RunAndReturn(run func(*contextmodel.ReqContext, string) error) *MockK8sClients_RemoveStar_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockK8sClients creates a new instance of MockK8sClients. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockK8sClients(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockK8sClients {
|
||||
mock := &MockK8sClients{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -26,14 +26,19 @@ func TestIntegrationStars(t *testing.T) {
|
||||
|
||||
for _, mode := range []grafanarest.DualWriterMode{
|
||||
grafanarest.Mode0,
|
||||
grafanarest.Mode2, // anything past 2 will fail
|
||||
grafanarest.Mode2,
|
||||
grafanarest.Mode3,
|
||||
grafanarest.Mode5,
|
||||
} {
|
||||
flags := []string{featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs}
|
||||
if mode > grafanarest.Mode2 {
|
||||
flags = append(flags, featuremgmt.FlagKubernetesStars)
|
||||
}
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||
},
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
DisableAnonymous: true,
|
||||
EnableFeatureToggles: flags,
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {
|
||||
DualWriterMode: mode,
|
||||
|
||||
Reference in New Issue
Block a user