diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2bf402c41e0..f92396c8e8a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -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; diff --git a/pkg/registry/apis/preferences/register.go b/pkg/registry/apis/preferences/register.go index 2c3d3623c62..6fff343f233 100644 --- a/pkg/registry/apis/preferences/register.go +++ b/pkg/registry/apis/preferences/register.go @@ -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 { diff --git a/pkg/registry/apis/preferences/stars.go b/pkg/registry/apis/preferences/stars.go new file mode 100644 index 00000000000..68bbb5c61bd --- /dev/null +++ b/pkg/registry/apis/preferences/stars.go @@ -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) +} diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 27ca946912c..6b99759d59a 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -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) diff --git a/pkg/services/apiserver/restconfig.go b/pkg/services/apiserver/restconfig.go index ee822e1917e..e3fdbfe7dd9 100644 --- a/pkg/services/apiserver/restconfig.go +++ b/pkg/services/apiserver/restconfig.go @@ -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 { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5ceeb8b3166..dfe68f5ecf5 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4a3c802aea5..2df0d029c26 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 6f3813ead1c..033e8990946 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index cb15026bfd3..bc11ea33d02 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/pkg/services/star/api/api.go b/pkg/services/star/api/api.go index 42110ba2c09..32d6cc08ab6 100644 --- a/pkg/services/star/api/api.go +++ b/pkg/services/star/api/api.go @@ -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) diff --git a/pkg/services/star/api/api_test.go b/pkg/services/star/api/api_test.go index 1469ef46c9a..6912b5c8b7e 100644 --- a/pkg/services/star/api/api_test.go +++ b/pkg/services/star/api/api_test.go @@ -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 diff --git a/pkg/services/star/api/client.go b/pkg/services/star/api/client.go new file mode 100644 index 00000000000..2f4cd238cc7 --- /dev/null +++ b/pkg/services/star/api/client.go @@ -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() +} diff --git a/pkg/services/star/api/client_mock.go b/pkg/services/star/api/client_mock.go new file mode 100644 index 00000000000..ecebf50dbfb --- /dev/null +++ b/pkg/services/star/api/client_mock.go @@ -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 +} diff --git a/pkg/tests/apis/preferences/stars_test.go b/pkg/tests/apis/preferences/stars_test.go index 93106383cfa..3efc2469119 100644 --- a/pkg/tests/apis/preferences/stars_test.go +++ b/pkg/tests/apis/preferences/stars_test.go @@ -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,