DualWriter: Support managed DualWriter (#100881)
This commit is contained in:
@@ -210,6 +210,7 @@ export interface FeatureToggles {
|
||||
unifiedStorageSearch?: boolean;
|
||||
unifiedStorageSearchSprinkles?: boolean;
|
||||
unifiedStorageSearchPermissionFiltering?: boolean;
|
||||
managedDualWriter?: boolean;
|
||||
pluginsSriChecks?: boolean;
|
||||
unifiedStorageBigObjectsSupport?: boolean;
|
||||
timeRangeProvider?: boolean;
|
||||
|
||||
@@ -62,6 +62,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
)
|
||||
@@ -833,11 +834,13 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
quotaService := quotatest.New(false, nil)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil,
|
||||
dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
if dashboardService == nil {
|
||||
dashboardService, err = service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions,
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, sort.ProvideService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -846,7 +849,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions,
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
sort.ProvideService(),
|
||||
dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
)
|
||||
@@ -463,7 +464,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
fStore := folderimpl.ProvideStore(sc.db)
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
acSvc := acimpl.ProvideOSSService(
|
||||
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
|
||||
features, tracing.InitializeTracerForTest(), sc.db, permreg.ProvidePermissionRegistry(), nil,
|
||||
@@ -474,7 +475,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
sc.cfg, dashStore, folderStore,
|
||||
features, folderPermissions, ac,
|
||||
folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, sort.ProvideService(),
|
||||
folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(b, err)
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
@@ -22,7 +26,6 @@ import (
|
||||
type MigrateOptions struct {
|
||||
Namespace string
|
||||
Store resource.BatchStoreClient
|
||||
Writer resource.BatchResourceWriter
|
||||
LargeObjects apistore.LargeObjectSupport
|
||||
BlobStore resource.BlobStoreClient
|
||||
Resources []schema.GroupResource
|
||||
@@ -36,6 +39,15 @@ type LegacyMigrator interface {
|
||||
Migrate(ctx context.Context, opts MigrateOptions) (*resource.BatchResponse, error)
|
||||
}
|
||||
|
||||
// This can migrate Folders, Dashboards and LibraryPanels
|
||||
func ProvideLegacyMigrator(
|
||||
sql db.DB, // direct access to tables
|
||||
provisioning provisioning.ProvisioningService, // only needed for dashboard settings
|
||||
) LegacyMigrator {
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, false, sort.ProvideService())
|
||||
}
|
||||
|
||||
type BlobStoreInfo struct {
|
||||
Count int64
|
||||
Size int64
|
||||
@@ -49,6 +61,9 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts.Progress == nil {
|
||||
opts.Progress = func(count int, msg string) {} // noop
|
||||
}
|
||||
|
||||
// Migrate everything
|
||||
if len(opts.Resources) < 1 {
|
||||
|
||||
@@ -209,7 +209,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: sortByField,
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "", // sort by should be empty if title is what we sorted by
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
@@ -144,7 +144,7 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "views_total",
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
@@ -204,7 +204,7 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "errors_last_30_days",
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
@@ -30,7 +31,6 @@ import (
|
||||
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
foldermodel "github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/util/errhttp"
|
||||
)
|
||||
@@ -43,8 +43,8 @@ type SearchHandler struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
}
|
||||
|
||||
func NewSearchHandler(tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient, resourceClient resource.ResourceClient, features featuremgmt.FeatureToggles) *SearchHandler {
|
||||
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, resourceClient, legacyDashboardSearcher)
|
||||
func NewSearchHandler(tracer trace.Tracer, dual dualwrite.Service, legacyDashboardSearcher resource.ResourceIndexClient, resourceClient resource.ResourceClient, features featuremgmt.FeatureToggles) *SearchHandler {
|
||||
searchClient := resource.NewSearchClient(dual, dashboard.DashboardResourceInfo.GroupResource(), resourceClient, legacyDashboardSearcher)
|
||||
return &SearchHandler{
|
||||
client: searchClient,
|
||||
log: log.New("grafana-apiserver.dashboards.search"),
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
@@ -33,8 +34,8 @@ func TestSearchFallback(t *testing.T) {
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0},
|
||||
},
|
||||
}
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil)
|
||||
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient)
|
||||
dual := dualwrite.ProvideService(featuremgmt.WithFeatures(), nil, cfg)
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), dual, mockLegacyClient, mockClient, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
@@ -60,8 +61,8 @@ func TestSearchFallback(t *testing.T) {
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1},
|
||||
},
|
||||
}
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil)
|
||||
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient)
|
||||
dual := dualwrite.ProvideService(featuremgmt.WithFeatures(), nil, cfg)
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), dual, mockLegacyClient, mockClient, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
@@ -87,8 +88,8 @@ func TestSearchFallback(t *testing.T) {
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2},
|
||||
},
|
||||
}
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil)
|
||||
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient)
|
||||
dual := dualwrite.ProvideService(featuremgmt.WithFeatures(), nil, cfg)
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), dual, mockLegacyClient, mockClient, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
@@ -114,8 +115,8 @@ func TestSearchFallback(t *testing.T) {
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3},
|
||||
},
|
||||
}
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil)
|
||||
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient)
|
||||
dual := dualwrite.ProvideService(featuremgmt.WithFeatures(), nil, cfg)
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), dual, mockLegacyClient, mockClient, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
@@ -141,8 +142,8 @@ func TestSearchFallback(t *testing.T) {
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4},
|
||||
},
|
||||
}
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil)
|
||||
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient)
|
||||
dual := dualwrite.ProvideService(featuremgmt.WithFeatures(), nil, cfg)
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), dual, mockLegacyClient, mockClient, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
@@ -168,8 +169,8 @@ func TestSearchFallback(t *testing.T) {
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5},
|
||||
},
|
||||
}
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient, mockClient, nil)
|
||||
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockClient, mockLegacyClient)
|
||||
dual := dualwrite.ProvideService(featuremgmt.WithFeatures(), nil, cfg)
|
||||
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), dual, mockLegacyClient, mockClient, nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/search", nil)
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
@@ -69,6 +70,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
sql db.DB,
|
||||
tracing *tracing.TracingService,
|
||||
unified resource.ResourceClient,
|
||||
dual dualwrite.Service,
|
||||
sorter sort.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||
@@ -84,7 +86,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
features: features,
|
||||
accessControl: accessControl,
|
||||
unified: unified,
|
||||
search: dashboard.NewSearchHandler(tracing, cfg, legacyDashboardSearcher, unified, features),
|
||||
search: dashboard.NewSearchHandler(tracing, dual, legacyDashboardSearcher, unified, features),
|
||||
|
||||
legacy: &dashboard.DashboardStorage{
|
||||
Resource: dashboardv0alpha1.DashboardResourceInfo,
|
||||
|
||||
+5
-3
@@ -9,10 +9,7 @@ package server
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/api/avatar"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
@@ -38,6 +35,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware/csrf"
|
||||
"github.com/grafana/grafana/pkg/middleware/loggermw"
|
||||
apiregistry "github.com/grafana/grafana/pkg/registry/apis"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
appregistry "github.com/grafana/grafana/pkg/registry/apps"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
@@ -159,6 +157,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
legacydualwrite "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
unifiedsearch "github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||
cloudmonitoring "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring"
|
||||
@@ -204,6 +204,7 @@ var wireBasicSet = wire.NewSet(
|
||||
uss.ProvideService,
|
||||
wire.Bind(new(usagestats.Service), new(*uss.UsageStats)),
|
||||
validator.ProvideService,
|
||||
legacy.ProvideLegacyMigrator,
|
||||
pluginsintegration.WireSet,
|
||||
pluginDashboards.ProvideFileStoreManager,
|
||||
wire.Bind(new(pluginDashboards.FileStore), new(*pluginDashboards.FileStoreManager)),
|
||||
@@ -214,6 +215,7 @@ var wireBasicSet = wire.NewSet(
|
||||
mysql.ProvideService,
|
||||
mssql.ProvideService,
|
||||
store.ProvideEntityEventsService,
|
||||
legacydualwrite.ProvideService,
|
||||
httpclientprovider.New,
|
||||
wire.Bind(new(httpclient.Provider), new(*sdkhttpclient.Provider)),
|
||||
serverlock.ProvideService,
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
func ProvideFolderPermissions(
|
||||
@@ -48,7 +49,7 @@ func ProvideFolderPermissions(
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||
fService := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
acSvc := acimpl.ProvideOSSService(
|
||||
cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(),
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
@@ -51,9 +52,9 @@ func TestIntegrationAuthorize(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, sort.ProvideService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
@@ -63,9 +64,9 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, sort.ProvideService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc)
|
||||
@@ -246,9 +247,9 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, sort.ProvideService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
cfg.AnnotationMaximumTagsLength = 60
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
)
|
||||
|
||||
@@ -275,6 +276,7 @@ func InstallAPIs(
|
||||
namespaceMapper request.NamespaceMapper,
|
||||
kvStore grafanarest.NamespacedKVStore,
|
||||
serverLock ServerLockService,
|
||||
dualWriteService dualwrite.Service,
|
||||
optsregister apistore.StorageOptionsRegister,
|
||||
) error {
|
||||
// dual writing is only enabled when the storage type is not legacy.
|
||||
@@ -285,6 +287,11 @@ func InstallAPIs(
|
||||
// nolint:staticcheck
|
||||
if storageOpts.StorageType != options.StorageTypeLegacy {
|
||||
dualWrite = func(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) {
|
||||
// Dashboards + Folders may be managed (depends on feature toggles and database state)
|
||||
if dualWriteService != nil && dualWriteService.ShouldManage(gr) {
|
||||
return dualWriteService.NewStorage(gr, legacy, storage) // eventually this can replace this whole function
|
||||
}
|
||||
|
||||
key := gr.String() // ${resource}.{group} eg playlists.playlist.grafana.app
|
||||
|
||||
// Get the option from custom.ini/command line
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
@@ -51,11 +51,10 @@ type k8sHandler struct {
|
||||
userService user.Service
|
||||
}
|
||||
|
||||
func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource,
|
||||
func NewK8sHandler(dual dualwrite.Service, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource,
|
||||
restConfig func(context.Context) *rest.Config, dashStore dashboards.Store, userSvc user.Service, resourceClient resource.ResourceClient, sorter sort.Service) K8sHandler {
|
||||
legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
key := gvr.Resource + "." + gvr.Group // the unified storage key in the config.ini is resource + group
|
||||
searchClient := resource.NewSearchClient(cfg, key, resourceClient, legacySearcher)
|
||||
searchClient := resource.NewSearchClient(dual, gvr.GroupResource(), resourceClient, legacySearcher)
|
||||
|
||||
return &k8sHandler{
|
||||
namespacer: namespacer,
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
@@ -137,6 +138,7 @@ type service struct {
|
||||
|
||||
authorizer *authorizer.GrafanaAuthorizer
|
||||
serverLockService builder.ServerLockService
|
||||
storageStatus dualwrite.Service
|
||||
kvStore kvstore.KVStore
|
||||
|
||||
pluginClient plugins.Client
|
||||
@@ -161,6 +163,7 @@ func ProvideService(
|
||||
datasources datasource.ScopedPluginDatasourceProvider,
|
||||
contextProvider datasource.PluginContextWrapper,
|
||||
pluginStore pluginstore.Store,
|
||||
storageStatus dualwrite.Service,
|
||||
unified resource.ResourceClient,
|
||||
buildHandlerChainFuncFromBuilders builder.BuildHandlerChainFuncFromBuilders,
|
||||
) (*service, error) {
|
||||
@@ -181,6 +184,7 @@ func ProvideService(
|
||||
contextProvider: contextProvider,
|
||||
pluginStore: pluginStore,
|
||||
serverLockService: serverLockService,
|
||||
storageStatus: storageStatus,
|
||||
unified: unified,
|
||||
buildHandlerChainFuncFromBuilders: buildHandlerChainFuncFromBuilders,
|
||||
}
|
||||
@@ -370,6 +374,7 @@ func (s *service) start(ctx context.Context) error {
|
||||
// Required for the dual writer initialization
|
||||
s.metrics, request.GetNamespaceMapper(s.cfg), kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"),
|
||||
s.serverLockService,
|
||||
s.storageStatus,
|
||||
optsregister,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
var testFeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
|
||||
@@ -303,7 +304,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore),
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
parentUID := ""
|
||||
for i := 0; ; i++ {
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -929,7 +930,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
user := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
@@ -1049,7 +1050,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
|
||||
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
user := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -23,9 +22,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
@@ -54,6 +51,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -96,9 +94,9 @@ func ProvideDashboardServiceImpl(
|
||||
ac accesscontrol.AccessControl, folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service,
|
||||
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
|
||||
resourceClient resource.ResourceClient, sorter sort.Service,
|
||||
resourceClient resource.ResourceClient, dual dualwrite.Service, sorter sort.Service,
|
||||
) (*DashboardServiceImpl, error) {
|
||||
k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, resourceClient, sorter)
|
||||
k8sHandler := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, resourceClient, sorter)
|
||||
|
||||
dashSvc := &DashboardServiceImpl{
|
||||
cfg: cfg,
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
@@ -898,6 +899,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
dashboardPermissions := accesscontrolmock.NewMockedPermissionsService()
|
||||
@@ -915,6 +917,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -989,6 +992,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
dashboardPermissions := accesscontrolmock.NewMockedPermissionsService()
|
||||
@@ -1008,6 +1012,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1043,6 +1048,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
service, err := ProvideDashboardServiceImpl(
|
||||
@@ -1059,6 +1065,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1113,6 +1120,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
service, err := ProvideDashboardServiceImpl(
|
||||
@@ -1129,6 +1137,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -1189,6 +1198,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
|
||||
@@ -1206,6 +1216,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
@@ -118,6 +119,7 @@ func TestValidateDashboardExists(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
@@ -42,7 +43,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, features featuremgmt.FeatureToggles,
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, sorter sort.Service) dashver.Service {
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, dual dualwrite.Service, sorter sort.Service) dashver.Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
store: &sqlStore{
|
||||
@@ -51,7 +52,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.Dash
|
||||
},
|
||||
features: features,
|
||||
k8sclient: client.NewK8sHandler(
|
||||
cfg,
|
||||
dual,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
v0alpha1.DashboardResourceInfo.GroupVersionResource(),
|
||||
restConfigProvider.GetRestConfig,
|
||||
|
||||
@@ -1454,6 +1454,14 @@ var (
|
||||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "managedDualWriter",
|
||||
Description: "Pick the dual write mode from database configs",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "pluginsSriChecks",
|
||||
Description: "Enables SRI checks for plugin assets",
|
||||
|
||||
@@ -191,6 +191,7 @@ rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false
|
||||
unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false
|
||||
unifiedStorageSearchSprinkles,experimental,@grafana/search-and-storage,false,false,false
|
||||
unifiedStorageSearchPermissionFiltering,experimental,@grafana/search-and-storage,false,false,false
|
||||
managedDualWriter,experimental,@grafana/search-and-storage,false,false,false
|
||||
pluginsSriChecks,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||
unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false
|
||||
timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||
|
||||
|
@@ -775,6 +775,10 @@ const (
|
||||
// Enable permission filtering on unified storage search
|
||||
FlagUnifiedStorageSearchPermissionFiltering = "unifiedStorageSearchPermissionFiltering"
|
||||
|
||||
// FlagManagedDualWriter
|
||||
// Pick the dual write mode from database configs
|
||||
FlagManagedDualWriter = "managedDualWriter"
|
||||
|
||||
// FlagPluginsSriChecks
|
||||
// Enables SRI checks for plugin assets
|
||||
FlagPluginsSriChecks = "pluginsSriChecks"
|
||||
|
||||
@@ -2621,6 +2621,20 @@
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "managedDualWriter",
|
||||
"resourceVersion": "1739347092893",
|
||||
"creationTimestamp": "2025-02-12T07:58:12Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Pick the dual write mode from database configs",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/search-and-storage",
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "managedPluginsInstall",
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -88,6 +89,7 @@ func ProvideService(
|
||||
r prometheus.Registerer,
|
||||
tracer tracing.Tracer,
|
||||
resourceClient resource.ResourceClient,
|
||||
dual dualwrite.Service,
|
||||
sorter sort.Service,
|
||||
) *Service {
|
||||
srv := &Service{
|
||||
@@ -113,7 +115,7 @@ func ProvideService(
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
k8sHandler := client.NewK8sHandler(
|
||||
cfg,
|
||||
dual,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
v0alpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
apiserver.GetRestConfig,
|
||||
@@ -131,7 +133,7 @@ func ProvideService(
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
dashHandler := client.NewK8sHandler(
|
||||
cfg,
|
||||
dual,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
dashboardalpha1.DashboardResourceInfo.GroupVersionResource(),
|
||||
apiserver.GetRestConfig,
|
||||
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -68,7 +69,7 @@ func TestIntegrationProvideFolderService(t *testing.T) {
|
||||
store := ProvideStore(db)
|
||||
ProvideService(
|
||||
store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
nil, nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2)
|
||||
})
|
||||
@@ -496,7 +497,8 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
})
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, sort.ProvideService())
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil,
|
||||
client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -582,7 +584,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff,
|
||||
folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, sort.ProvideService())
|
||||
folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
|
||||
@@ -725,7 +727,9 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
tc.service.store = nestedFolderStore
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, sort.ProvideService())
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service,
|
||||
tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil,
|
||||
dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -1518,6 +1522,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
@@ -183,7 +184,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
features := featuremgmt.WithFeatures(featuresArr...)
|
||||
|
||||
dashboardStore := dashboards.NewFakeDashboardStore(t)
|
||||
k8sCli := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService, nil, sort.ProvideService())
|
||||
k8sCli := client.NewK8sHandler(dualwrite.ProvideTestService(), request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService, nil, sort.ProvideService())
|
||||
unifiedStore := ProvideUnifiedStore(k8sCli, userService)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -348,7 +349,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
_, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{UID: folderUID, SignedInUser: &user, Title: folderUID + "-title"})
|
||||
require.NoError(t, err)
|
||||
service, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
@@ -363,6 +364,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -387,7 +389,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc fold
|
||||
store := folderimpl.ProvideStore(sc.sqlStore)
|
||||
folderSvc = folderimpl.ProvideService(
|
||||
store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
t.Logf("Creating folder with title %q and UID uid_for_%s", title, title)
|
||||
}
|
||||
ctx := identity.WithRequester(context.Background(), &sc.user)
|
||||
@@ -451,12 +453,12 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, folderPermissions, ac,
|
||||
folderSvc, fStore,
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, sort.ProvideService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, svcErr)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -520,7 +522,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
|
||||
require.NoError(t, err)
|
||||
err = folderSvc.RegisterService(alertStore)
|
||||
@@ -529,7 +531,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, folderPermissions, ac,
|
||||
folderSvc, fStore,
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, sort.ProvideService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, dashSvcErr)
|
||||
dashService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -42,6 +42,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
@@ -737,7 +738,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, acmock.NewMockedPermissionsService(), ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, sort.ProvideService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
service.RegisterDashboardPermissions(dashPermissionService)
|
||||
@@ -759,7 +760,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
|
||||
fStore := folderimpl.ProvideStore(sc.sqlStore)
|
||||
s := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
t.Logf("Creating folder with title and UID %q", title)
|
||||
ctx := identity.WithRequester(context.Background(), sc.user)
|
||||
@@ -835,7 +836,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
cfg, dashStore, folderStore,
|
||||
features, acmock.NewMockedPermissionsService(), ac,
|
||||
folderSvc, folder.NewFakeStore(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, sort.ProvideService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(dashPermissionService)
|
||||
@@ -847,7 +848,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
|
||||
folderService := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac, dashService)
|
||||
service := LibraryPanelService{
|
||||
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -1922,7 +1923,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderService := folderimpl.ProvideService(
|
||||
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
store := store.DBstore{
|
||||
Logger: log,
|
||||
SQLStore: sqlStore,
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
@@ -1607,7 +1608,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderService := folderimpl.ProvideService(
|
||||
fStore, ac, inProcBus, dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
ruleService := createAlertRuleService(t, folderService)
|
||||
var orgID int64 = 1
|
||||
|
||||
@@ -27,13 +27,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) folder.Service {
|
||||
tb.Helper()
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, nil, db,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
}
|
||||
|
||||
func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) {
|
||||
@@ -65,7 +66,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa
|
||||
features, folderPermissions, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
sort.ProvideService(),
|
||||
dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(tb, err)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -4,12 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
// DashboardProvisioner is responsible for syncing dashboard from disk to
|
||||
@@ -24,7 +27,7 @@ type DashboardProvisioner interface {
|
||||
}
|
||||
|
||||
// DashboardProvisionerFactory creates DashboardProvisioners based on input
|
||||
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service) (DashboardProvisioner, error)
|
||||
type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (DashboardProvisioner, error)
|
||||
|
||||
// Provisioner is responsible for syncing dashboard from disk to Grafana's database.
|
||||
type Provisioner struct {
|
||||
@@ -33,6 +36,7 @@ type Provisioner struct {
|
||||
configs []*config
|
||||
duplicateValidator duplicateValidator
|
||||
provisioner dashboards.DashboardProvisioningService
|
||||
dual dualwrite.Service
|
||||
}
|
||||
|
||||
func (provider *Provisioner) HasDashboardSources() bool {
|
||||
@@ -40,7 +44,7 @@ func (provider *Provisioner) HasDashboardSources() bool {
|
||||
}
|
||||
|
||||
// New returns a new DashboardProvisioner
|
||||
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service) (DashboardProvisioner, error) {
|
||||
func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service) (DashboardProvisioner, error) {
|
||||
logger := log.New("provisioning.dashboard")
|
||||
cfgReader := &configReader{path: configDirectory, log: logger, orgExists: utils.NewOrgExistsChecker(orgService)}
|
||||
configs, err := cfgReader.readConfig(ctx)
|
||||
@@ -53,12 +57,17 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
|
||||
return nil, fmt.Errorf("%v: %w", "Failed to initialize file readers", err)
|
||||
}
|
||||
|
||||
if dual != nil && !dual.ShouldManage(dashboard.DashboardResourceInfo.GroupResource()) {
|
||||
dual = nil // not activily managed
|
||||
}
|
||||
|
||||
d := &Provisioner{
|
||||
log: logger,
|
||||
fileReaders: fileReaders,
|
||||
configs: configs,
|
||||
duplicateValidator: newDuplicateValidator(logger, fileReaders),
|
||||
provisioner: provisioner,
|
||||
dual: dual,
|
||||
}
|
||||
|
||||
return d, nil
|
||||
@@ -67,6 +76,15 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
|
||||
// Provision scans the disk for dashboards and updates
|
||||
// the database with the latest versions of those dashboards.
|
||||
func (provider *Provisioner) Provision(ctx context.Context) error {
|
||||
// skip provisioning during migrations to prevent multi-replica instances from crashing when another replica is migrating
|
||||
if provider.dual != nil {
|
||||
status, _ := provider.dual.Status(context.Background(), dashboard.DashboardResourceInfo.GroupResource())
|
||||
if status.Migrating > 0 {
|
||||
provider.log.Info("dashboard migrations are running, skipping provisioning", "elapsed", time.Since(time.UnixMilli(status.Migrating)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
provider.log.Info("starting to provision dashboards")
|
||||
|
||||
for _, reader := range provider.fileReaders {
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -132,7 +133,7 @@ func TestDashboardFileReader(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
t.Run("Reading dashboards from disk", func(t *testing.T) {
|
||||
t.Run("Can read default dashboard", func(t *testing.T) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
grafanasort "github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -51,7 +52,7 @@ func TestDuplicatesValidator(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, grafanasort.ProvideService())
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), grafanasort.ProvideService())
|
||||
|
||||
t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) {
|
||||
const folderName = "duplicates-validator-folder"
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
func ProvideService(
|
||||
@@ -57,6 +58,7 @@ func ProvideService(
|
||||
orgService org.Service,
|
||||
resourcePermissions accesscontrol.ReceiverPermissionsService,
|
||||
tracer tracing.Tracer,
|
||||
dual dualwrite.Service,
|
||||
) (*ProvisioningServiceImpl, error) {
|
||||
s := &ProvisioningServiceImpl{
|
||||
Cfg: cfg,
|
||||
@@ -94,7 +96,7 @@ func ProvideService(
|
||||
|
||||
func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error {
|
||||
dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards")
|
||||
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService)
|
||||
dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: %w", "Failed to create provisioner", err)
|
||||
}
|
||||
@@ -164,6 +166,7 @@ type ProvisioningServiceImpl struct {
|
||||
folderService folder.Service
|
||||
resourcePermissions accesscontrol.ReceiverPermissionsService
|
||||
tracer tracing.Tracer
|
||||
dual dualwrite.Service
|
||||
onceInitProvisioners sync.Once
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
func TestProvisioningServiceImpl(t *testing.T) {
|
||||
@@ -159,7 +160,7 @@ func setup(t *testing.T) *serviceTestStruct {
|
||||
searchStub := searchV2.NewStubSearchService()
|
||||
|
||||
service, err := newProvisioningServiceImpl(
|
||||
func(context.Context, string, dashboardstore.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service) (dashboards.DashboardProvisioner, error) {
|
||||
func(context.Context, string, dashboardstore.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (dashboards.DashboardProvisioner, error) {
|
||||
serviceTest.dashboardProvisionerInstantiations++
|
||||
return serviceTest.mock, nil
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -328,7 +329,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
|
||||
cfg, dashboardStoreService, folderStore,
|
||||
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil,
|
||||
nil, sort.ProvideService(),
|
||||
nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(dashPermissionService)
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -1398,9 +1399,9 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(testDB)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, sort.ProvideService())
|
||||
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{})
|
||||
fakeGuardian := &guardian.FakeDashboardGuardian{
|
||||
|
||||
@@ -54,6 +54,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -496,9 +497,9 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, sort.ProvideService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService())
|
||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
@@ -825,7 +826,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db),
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
// create parent folder
|
||||
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards, numFolders, nestingLevel int) {
|
||||
@@ -82,7 +83,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe
|
||||
fStore := folderimpl.ProvideStore(store)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store),
|
||||
nil, store, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, store, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
)
|
||||
|
||||
// Simple file implementation -- useful while testing and not yet sure about the SQL structure!
|
||||
// When a path exists, read/write it from disk; otherwise it is held in memory
|
||||
type fileDB struct {
|
||||
path string
|
||||
changed int64
|
||||
db map[string]StorageStatus
|
||||
mu sync.RWMutex
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
// File implementation while testing -- values are saved in the data directory
|
||||
func newFileDB(path string) *fileDB {
|
||||
return &fileDB{
|
||||
db: make(map[string]StorageStatus),
|
||||
path: path,
|
||||
logger: logging.DefaultLogger.With("logger", "fileDB"),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *fileDB) Get(ctx context.Context, gr schema.GroupResource) (StorageStatus, bool, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
info, err := os.Stat(m.path)
|
||||
if err == nil && info.ModTime().UnixMilli() != m.changed {
|
||||
v, err := os.ReadFile(m.path)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(v, &m.db)
|
||||
m.changed = info.ModTime().UnixMilli()
|
||||
}
|
||||
if err != nil {
|
||||
m.logger.Warn("error reading filedb", "err", err)
|
||||
}
|
||||
|
||||
changed := false
|
||||
for k, v := range m.db {
|
||||
// Must write to unified if we are reading unified
|
||||
if v.ReadUnified && !v.WriteUnified {
|
||||
v.WriteUnified = true
|
||||
m.db[k] = v
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Make sure we are writing something!
|
||||
if !(v.WriteLegacy || v.WriteUnified) {
|
||||
v.WriteLegacy = true
|
||||
m.db[k] = v
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
err = m.save()
|
||||
m.logger.Warn("error saving changes filedb", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
v, ok := m.db[gr.String()]
|
||||
return v, ok, nil
|
||||
}
|
||||
|
||||
func (m *fileDB) Set(ctx context.Context, status StorageStatus) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
gr := schema.GroupResource{
|
||||
Group: status.Group,
|
||||
Resource: status.Resource,
|
||||
}
|
||||
m.db[gr.String()] = status
|
||||
|
||||
return m.save()
|
||||
}
|
||||
|
||||
func (m *fileDB) save() error {
|
||||
if m.path != "" {
|
||||
data, err := json.MarshalIndent(m.db, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(m.path, data, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
)
|
||||
|
||||
func ProvideTestService(status ...StorageStatus) Service {
|
||||
if len(status) < 1 {
|
||||
status = []StorageStatus{{
|
||||
WriteLegacy: true,
|
||||
WriteUnified: false,
|
||||
Runtime: false,
|
||||
ReadUnified: false,
|
||||
}}
|
||||
}
|
||||
return &mockService{status: status[0]}
|
||||
}
|
||||
|
||||
type mockService struct {
|
||||
status StorageStatus
|
||||
}
|
||||
|
||||
// NewStorage implements Service.
|
||||
func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// ReadFromUnified implements Service.
|
||||
func (m *mockService) ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error) {
|
||||
return m.status.ReadUnified, nil
|
||||
}
|
||||
|
||||
// ShouldManage implements Service.
|
||||
func (m *mockService) ShouldManage(gr schema.GroupResource) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// StartMigration implements Service.
|
||||
func (m *mockService) StartMigration(ctx context.Context, gr schema.GroupResource, key int64) (StorageStatus, error) {
|
||||
return StorageStatus{}, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// Status implements Service.
|
||||
func (m *mockService) Status(ctx context.Context, gr schema.GroupResource) (StorageStatus, error) {
|
||||
s := m.status
|
||||
s.Group = gr.Group
|
||||
s.Resource = gr.Resource
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Update implements Service.
|
||||
func (m *mockService) Update(ctx context.Context, status StorageStatus) (StorageStatus, error) {
|
||||
return m.status, fmt.Errorf("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
)
|
||||
|
||||
func (m *service) NewStorage(gr schema.GroupResource,
|
||||
legacy grafanarest.LegacyStorage,
|
||||
storage grafanarest.Storage,
|
||||
) (grafanarest.Storage, error) {
|
||||
status, err := m.Status(context.Background(), gr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m.enabled && status.Runtime {
|
||||
// Dynamic storage behavior
|
||||
return &runtimeDualWriter{
|
||||
service: m,
|
||||
legacy: legacy,
|
||||
unified: storage,
|
||||
dualwrite: grafanarest.NewDualWriter(grafanarest.Mode3, legacy, storage, m.reg, gr.String()),
|
||||
gr: gr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if status.ReadUnified {
|
||||
if status.WriteLegacy {
|
||||
// Write both, read unified
|
||||
return grafanarest.NewDualWriter(grafanarest.Mode3, legacy, storage, m.reg, gr.String()), nil
|
||||
}
|
||||
return storage, nil
|
||||
}
|
||||
if status.WriteUnified {
|
||||
// Write both, read legacy
|
||||
return grafanarest.NewDualWriter(grafanarest.Mode2, legacy, storage, m.reg, gr.String()), nil
|
||||
}
|
||||
return legacy, nil
|
||||
}
|
||||
|
||||
// The runtime dual writer implements the various modes we have described as: mode:1/2/3/4/5
|
||||
// However the behavior can be configured at runtime rather than just at startup.
|
||||
// When a resource is marked as "migrating", all write requests will be 503 unavailable
|
||||
type runtimeDualWriter struct {
|
||||
service Service
|
||||
legacy grafanarest.LegacyStorage
|
||||
unified grafanarest.Storage
|
||||
dualwrite grafanarest.Storage
|
||||
gr schema.GroupResource
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
unified, err := d.service.ReadFromUnified(ctx, d.gr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unified {
|
||||
return d.unified.Get(ctx, name, options)
|
||||
}
|
||||
return d.legacy.Get(ctx, name, options)
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
unified, err := d.service.ReadFromUnified(ctx, d.gr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unified {
|
||||
return d.unified.List(ctx, options)
|
||||
}
|
||||
return d.legacy.List(ctx, options)
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) getWriter(ctx context.Context) (grafanarest.Storage, error) {
|
||||
status, err := d.service.Status(ctx, d.gr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.Migrating > 0 {
|
||||
return nil, &apierrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Code: http.StatusServiceUnavailable,
|
||||
Message: "the system is migrating",
|
||||
},
|
||||
}
|
||||
}
|
||||
if status.WriteLegacy {
|
||||
if status.WriteUnified {
|
||||
return d.dualwrite, nil
|
||||
}
|
||||
return d.legacy, nil // only write legacy (mode0)
|
||||
}
|
||||
return d.unified, nil // only write unified (mode4)
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) Create(ctx context.Context, in runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
store, err := d.getWriter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store.Create(ctx, in, createValidation, options)
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
store, err := d.getWriter(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return store.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
store, err := d.getWriter(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return store.Delete(ctx, name, deleteValidation, options)
|
||||
}
|
||||
|
||||
// DeleteCollection overrides the behavior of the generic DualWriter and deletes from both LegacyStorage and Storage.
|
||||
func (d *runtimeDualWriter) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
store, err := d.getWriter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) Destroy() {
|
||||
d.dualwrite.Destroy()
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) GetSingularName() string {
|
||||
return d.unified.GetSingularName()
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) NamespaceScoped() bool {
|
||||
return d.unified.NamespaceScoped()
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) New() runtime.Object {
|
||||
return d.unified.New()
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) NewList() runtime.Object {
|
||||
return d.unified.NewList()
|
||||
}
|
||||
|
||||
func (d *runtimeDualWriter) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return d.unified.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/apis/example"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
var now = time.Now()
|
||||
|
||||
var createFn = func(context.Context, runtime.Object) error { return nil }
|
||||
|
||||
var exampleObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "1", CreationTimestamp: metav1.Time{}, GenerateName: "foo"}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: now}}}
|
||||
var exampleObjNoRV = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "", CreationTimestamp: metav1.Time{}, GenerateName: "foo"}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: now}}}
|
||||
var anotherObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "2", GenerateName: "foo"}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: now}}}
|
||||
var failingObj = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "object-fail", ResourceVersion: "2", GenerateName: "object-fail"}, Spec: example.PodSpec{}, Status: example.PodStatus{}}
|
||||
|
||||
var p = prometheus.NewRegistry()
|
||||
var kind = schema.GroupResource{Group: "g", Resource: "r"}
|
||||
|
||||
func TestManagedMode3_Create(t *testing.T) {
|
||||
type testCase struct {
|
||||
input runtime.Object
|
||||
setupLegacyFn func(m *mock.Mock, input runtime.Object)
|
||||
setupStorageFn func(m *mock.Mock, input runtime.Object)
|
||||
name string
|
||||
wantErr bool
|
||||
}
|
||||
tests :=
|
||||
[]testCase{
|
||||
{
|
||||
name: "should succeed when creating an object in both the LegacyStorage and Storage",
|
||||
input: exampleObj,
|
||||
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
||||
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
||||
},
|
||||
setupStorageFn: func(m *mock.Mock, _ runtime.Object) {
|
||||
// We don't use the input here, as the input is transformed before being passed to unified storage.
|
||||
m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return an error when creating an object in the legacy store fails",
|
||||
input: failingObj,
|
||||
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
||||
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once()
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "should return an error when creating an object in the unified store fails and delete from LegacyStorage",
|
||||
input: exampleObj,
|
||||
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
||||
m.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, true, nil).Once()
|
||||
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
||||
},
|
||||
setupStorageFn: func(m *mock.Mock, _ runtime.Object) {
|
||||
// We don't use the input here, as the input is transformed before being passed to unified storage.
|
||||
m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once()
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := (rest.LegacyStorage)(nil)
|
||||
s := (rest.Storage)(nil)
|
||||
|
||||
ls := legacyStoreMock{&mock.Mock{}, l}
|
||||
us := storageMock{&mock.Mock{}, s}
|
||||
|
||||
if tt.setupLegacyFn != nil {
|
||||
tt.setupLegacyFn(ls.Mock, tt.input)
|
||||
}
|
||||
if tt.setupStorageFn != nil {
|
||||
tt.setupStorageFn(us.Mock, tt.input)
|
||||
}
|
||||
|
||||
m := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), p, nil)
|
||||
dw, err := m.NewStorage(kind, ls, us)
|
||||
require.NoError(t, err)
|
||||
|
||||
obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{})
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, exampleObj, obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedMode3_Get(t *testing.T) {
|
||||
type testCase struct {
|
||||
setupLegacyFn func(m *mock.Mock, name string)
|
||||
setupStorageFn func(m *mock.Mock, name string)
|
||||
name string
|
||||
wantErr bool
|
||||
}
|
||||
tests :=
|
||||
[]testCase{
|
||||
{
|
||||
name: "should succeed when getting an object from both stores",
|
||||
setupLegacyFn: func(m *mock.Mock, name string) {
|
||||
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
|
||||
},
|
||||
setupStorageFn: func(m *mock.Mock, name string) {
|
||||
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should return an error when getting an object in the unified store fails",
|
||||
setupLegacyFn: func(m *mock.Mock, name string) {
|
||||
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
|
||||
},
|
||||
setupStorageFn: func(m *mock.Mock, name string) {
|
||||
m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "should succeed when getting an object in the LegacyStorage fails",
|
||||
setupLegacyFn: func(m *mock.Mock, name string) {
|
||||
m.On("Get", mock.Anything, name, mock.Anything).Return(nil, errors.New("error"))
|
||||
},
|
||||
setupStorageFn: func(m *mock.Mock, name string) {
|
||||
m.On("Get", mock.Anything, name, mock.Anything).Return(exampleObj, nil)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
name := "foo"
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := (rest.LegacyStorage)(nil)
|
||||
s := (rest.Storage)(nil)
|
||||
|
||||
ls := legacyStoreMock{&mock.Mock{}, l}
|
||||
us := storageMock{&mock.Mock{}, s}
|
||||
|
||||
if tt.setupLegacyFn != nil {
|
||||
tt.setupLegacyFn(ls.Mock, name)
|
||||
}
|
||||
if tt.setupStorageFn != nil {
|
||||
tt.setupStorageFn(us.Mock, name)
|
||||
}
|
||||
|
||||
m := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), p, nil)
|
||||
dw, err := m.NewStorage(kind, ls, us)
|
||||
require.NoError(t, err)
|
||||
status, err := m.Status(context.Background(), kind)
|
||||
require.NoError(t, err)
|
||||
status.Migrated = now.UnixMilli()
|
||||
status.ReadUnified = true // Read from unified (like mode3)
|
||||
_, err = m.Update(context.Background(), status)
|
||||
require.NoError(t, err)
|
||||
|
||||
obj, err := dw.Get(context.Background(), name, &metav1.GetOptions{})
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, obj, exampleObj)
|
||||
require.NotEqual(t, obj, anotherObj)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedMode3_CreateWhileMigrating(t *testing.T) {
|
||||
type testCase struct {
|
||||
input runtime.Object
|
||||
setupLegacyFn func(m *mock.Mock, input runtime.Object)
|
||||
setupStorageFn func(m *mock.Mock, input runtime.Object)
|
||||
prepare func(dual Service) (StorageStatus, error)
|
||||
name string
|
||||
wantErr bool
|
||||
}
|
||||
tests :=
|
||||
[]testCase{
|
||||
{
|
||||
name: "should succeed when not migrated",
|
||||
input: exampleObj,
|
||||
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
||||
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
||||
},
|
||||
setupStorageFn: func(m *mock.Mock, _ runtime.Object) {
|
||||
// We don't use the input here, as the input is transformed before being passed to unified storage.
|
||||
m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
||||
},
|
||||
prepare: func(dual Service) (StorageStatus, error) {
|
||||
status, err := dual.Status(context.Background(), kind)
|
||||
require.NoError(t, err)
|
||||
status.Migrating = 0
|
||||
status.Migrated = 0
|
||||
return dual.Update(context.Background(), status)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should be unavailable if migrating",
|
||||
input: failingObj,
|
||||
wantErr: true,
|
||||
prepare: func(dual Service) (StorageStatus, error) {
|
||||
status, err := dual.Status(context.Background(), kind)
|
||||
require.NoError(t, err)
|
||||
return dual.StartMigration(context.Background(), kind, status.UpdateKey)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should succeed after migration",
|
||||
input: exampleObj,
|
||||
setupLegacyFn: func(m *mock.Mock, input runtime.Object) {
|
||||
m.On("Create", mock.Anything, input, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
||||
},
|
||||
setupStorageFn: func(m *mock.Mock, _ runtime.Object) {
|
||||
// We don't use the input here, as the input is transformed before being passed to unified storage.
|
||||
m.On("Create", mock.Anything, exampleObjNoRV, mock.Anything, mock.Anything).Return(exampleObj, nil).Once()
|
||||
},
|
||||
prepare: func(dual Service) (StorageStatus, error) {
|
||||
status, err := dual.Status(context.Background(), kind)
|
||||
require.NoError(t, err)
|
||||
status.Migrating = 0
|
||||
status.Migrated = now.UnixMilli()
|
||||
status.ReadUnified = true
|
||||
return dual.Update(context.Background(), status)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Shared provider across all tests
|
||||
dual := ProvideService(featuremgmt.WithFeatures(featuremgmt.FlagManagedDualWriter), p, nil)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := (rest.LegacyStorage)(nil)
|
||||
s := (rest.Storage)(nil)
|
||||
|
||||
ls := legacyStoreMock{&mock.Mock{}, l}
|
||||
us := storageMock{&mock.Mock{}, s}
|
||||
|
||||
if tt.setupLegacyFn != nil {
|
||||
tt.setupLegacyFn(ls.Mock, tt.input)
|
||||
}
|
||||
if tt.setupStorageFn != nil {
|
||||
tt.setupStorageFn(us.Mock, tt.input)
|
||||
}
|
||||
|
||||
dw, err := dual.NewStorage(kind, ls, us)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Apply the changes and
|
||||
if tt.prepare != nil {
|
||||
_, err = tt.prepare(dual)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
obj, err := dw.Create(context.Background(), tt.input, createFn, &metav1.CreateOptions{})
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, exampleObj, obj)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideService(features featuremgmt.FeatureToggles, reg prometheus.Registerer, cfg *setting.Cfg) Service {
|
||||
enabled := features.IsEnabledGlobally(featuremgmt.FlagManagedDualWriter) ||
|
||||
features.IsEnabledGlobally(featuremgmt.FlagProvisioning) // required for git provisioning
|
||||
if !enabled && cfg != nil {
|
||||
return &staticService{cfg} // fallback to using the dual write flags from cfg
|
||||
}
|
||||
|
||||
path := "" // storage path
|
||||
if cfg != nil {
|
||||
path = filepath.Join(cfg.DataPath, "dualwrite.json")
|
||||
}
|
||||
|
||||
return &service{
|
||||
db: newFileDB(path),
|
||||
reg: reg,
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
type service struct {
|
||||
db statusStorage
|
||||
reg prometheus.Registerer
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// The storage interface has zero business logic and simply writes values to a database
|
||||
type statusStorage interface {
|
||||
Get(ctx context.Context, gr schema.GroupResource) (StorageStatus, bool, error)
|
||||
Set(ctx context.Context, status StorageStatus) error
|
||||
}
|
||||
|
||||
// Hardcoded list of resources that should be controlled by the database (eventually everything?)
|
||||
func (m *service) ShouldManage(gr schema.GroupResource) bool {
|
||||
if !m.enabled {
|
||||
return false
|
||||
}
|
||||
switch gr.String() {
|
||||
case "folders.folder.grafana.app":
|
||||
return true
|
||||
case "dashboards.dashboard.grafana.app":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *service) ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error) {
|
||||
v, ok, err := m.db.Get(ctx, gr)
|
||||
return ok && v.ReadUnified, err
|
||||
}
|
||||
|
||||
// Status implements Service.
|
||||
func (m *service) Status(ctx context.Context, gr schema.GroupResource) (StorageStatus, error) {
|
||||
v, found, err := m.db.Get(ctx, gr)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if !found {
|
||||
v = StorageStatus{
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
WriteLegacy: true,
|
||||
WriteUnified: true, // Write both, but read legacy
|
||||
ReadUnified: false,
|
||||
Migrated: 0,
|
||||
Migrating: 0,
|
||||
Runtime: true, // need to explicitly ask for not runtime
|
||||
UpdateKey: 1,
|
||||
}
|
||||
err := m.db.Set(ctx, v)
|
||||
return v, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// StartMigration implements Service.
|
||||
func (m *service) StartMigration(ctx context.Context, gr schema.GroupResource, key int64) (StorageStatus, error) {
|
||||
now := time.Now().UnixMilli()
|
||||
v, ok, err := m.db.Get(ctx, gr)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if ok {
|
||||
if v.Migrated > 0 {
|
||||
return v, fmt.Errorf("already migrated")
|
||||
}
|
||||
if key != v.UpdateKey {
|
||||
return v, fmt.Errorf("migration key mismatch")
|
||||
}
|
||||
if v.Migrating > 0 {
|
||||
return v, fmt.Errorf("migration in progress")
|
||||
}
|
||||
|
||||
v.Migrating = now
|
||||
v.UpdateKey++
|
||||
} else {
|
||||
v = StorageStatus{
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
Runtime: true,
|
||||
WriteLegacy: true,
|
||||
WriteUnified: true,
|
||||
ReadUnified: false,
|
||||
Migrating: now,
|
||||
Migrated: 0, // timestamp
|
||||
UpdateKey: 1,
|
||||
}
|
||||
}
|
||||
err = m.db.Set(ctx, v)
|
||||
return v, err
|
||||
}
|
||||
|
||||
// FinishMigration implements Service.
|
||||
func (m *service) Update(ctx context.Context, status StorageStatus) (StorageStatus, error) {
|
||||
v, ok, err := m.db.Get(ctx, schema.GroupResource{Group: status.Group, Resource: status.Resource})
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
if !ok {
|
||||
return v, fmt.Errorf("unable to update status that is not yet saved")
|
||||
}
|
||||
if status.UpdateKey != v.UpdateKey {
|
||||
return v, fmt.Errorf("key mismatch (resource: %s, expected:%d, received: %d)", v.Resource, v.UpdateKey, status.UpdateKey)
|
||||
}
|
||||
if status.Migrating > 0 {
|
||||
return v, fmt.Errorf("update can not change migrating status")
|
||||
}
|
||||
if status.ReadUnified {
|
||||
if status.Migrated == 0 {
|
||||
return v, fmt.Errorf("can not read from unified before a migration")
|
||||
}
|
||||
if !status.WriteUnified {
|
||||
return v, fmt.Errorf("must write to unified when reading from unified")
|
||||
}
|
||||
}
|
||||
if !status.WriteLegacy && !status.WriteUnified {
|
||||
return v, fmt.Errorf("must write either legacy or unified")
|
||||
}
|
||||
status.UpdateKey++
|
||||
return status, m.db.Set(ctx, status)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mode := ProvideService(featuremgmt.WithFeatures(), nil, nil)
|
||||
|
||||
gr := schema.GroupResource{Group: "ggg", Resource: "rrr"}
|
||||
status, err := mode.Status(ctx, gr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StorageStatus{
|
||||
Group: "ggg",
|
||||
Resource: "rrr",
|
||||
WriteLegacy: true,
|
||||
WriteUnified: true,
|
||||
ReadUnified: false,
|
||||
Migrated: 0,
|
||||
Migrating: 0,
|
||||
Runtime: true,
|
||||
UpdateKey: 1,
|
||||
}, status, "should start with the right defaults")
|
||||
|
||||
// Start migration
|
||||
status, err = mode.StartMigration(ctx, gr, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, status.UpdateKey, int64(2), "the key increased")
|
||||
require.True(t, status.Migrating > 0, "migration is running")
|
||||
|
||||
status.Migrated = time.Now().UnixMilli()
|
||||
status.Migrating = 0
|
||||
status, err = mode.Update(ctx, status)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, status.UpdateKey, int64(3), "the key increased")
|
||||
require.Equal(t, status.Migrating, int64(0), "done migrating")
|
||||
require.True(t, status.Migrated > 0, "migration is running")
|
||||
|
||||
status.WriteUnified = false
|
||||
status.ReadUnified = true
|
||||
_, err = mode.Update(ctx, status)
|
||||
require.Error(t, err) // must write unified if we read it
|
||||
|
||||
status.WriteUnified = false
|
||||
status.ReadUnified = false
|
||||
status.WriteLegacy = false
|
||||
_, err = mode.Update(ctx, status)
|
||||
require.Error(t, err) // must write something!
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package dualwrite
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
// Not yet used... but you get the idea
|
||||
func AddUnifiedStatusMigrations(mg *migrator.Migrator) {
|
||||
resourceStorageStatus := migrator.Table{
|
||||
Name: "resource_storage_status",
|
||||
Columns: []*migrator.Column{
|
||||
{Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "write_legacy", Type: migrator.DB_Bool, Nullable: false, Default: "TRUE"},
|
||||
{Name: "write_unified", Type: migrator.DB_Bool, Nullable: false, Default: "TRUE"},
|
||||
{Name: "read_unified", Type: migrator.DB_Bool, Nullable: false},
|
||||
{Name: "migrating", Type: migrator.DB_BigInt, Nullable: false}, // Timestamp Actively running a migration (start timestamp)
|
||||
{Name: "migrated", Type: migrator.DB_BigInt, Nullable: false}, // Timestamp job finished
|
||||
{Name: "runtime", Type: migrator.DB_Bool, Nullable: false, Default: "TRUE"},
|
||||
{Name: "update_key", Type: migrator.DB_BigInt, Nullable: false}, // optimistic lock key -- required for update
|
||||
},
|
||||
Indices: []*migrator.Index{
|
||||
{Cols: []string{"group", "resource"}, Type: migrator.UniqueIndex},
|
||||
},
|
||||
}
|
||||
mg.AddMigration("create resource_storage_status table", migrator.NewAddTableMigration(resourceStorageStatus))
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type staticService struct {
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// ReadFromUnified implements Service.
|
||||
func (m *staticService) ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error) {
|
||||
config := m.cfg.UnifiedStorage[gr.String()]
|
||||
switch config.DualWriterMode {
|
||||
case rest.Mode3, rest.Mode4, rest.Mode5:
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldManage implements Service.
|
||||
func (m *staticService) ShouldManage(gr schema.GroupResource) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// StartMigration implements Service.
|
||||
func (m *staticService) StartMigration(ctx context.Context, gr schema.GroupResource, key int64) (StorageStatus, error) {
|
||||
return StorageStatus{}, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// Status implements Service.
|
||||
func (m *staticService) Status(ctx context.Context, gr schema.GroupResource) (StorageStatus, error) {
|
||||
status := StorageStatus{
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
WriteLegacy: true,
|
||||
}
|
||||
config, ok := m.cfg.UnifiedStorage[gr.String()]
|
||||
if ok {
|
||||
switch config.DualWriterMode {
|
||||
case rest.Mode0:
|
||||
status.WriteLegacy = true
|
||||
status.WriteUnified = false
|
||||
status.ReadUnified = false
|
||||
case rest.Mode1, rest.Mode2: // only difference is that 2 will error!
|
||||
status.WriteLegacy = true
|
||||
status.WriteUnified = true
|
||||
status.ReadUnified = false
|
||||
case rest.Mode3:
|
||||
status.WriteLegacy = true
|
||||
status.WriteUnified = true
|
||||
status.ReadUnified = true
|
||||
case rest.Mode4, rest.Mode5:
|
||||
status.WriteLegacy = false
|
||||
status.WriteUnified = true
|
||||
status.ReadUnified = true
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// Update implements Service.
|
||||
func (m *staticService) Update(ctx context.Context, status StorageStatus) (StorageStatus, error) {
|
||||
return StorageStatus{}, fmt.Errorf("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
)
|
||||
|
||||
type legacyStoreMock struct {
|
||||
*mock.Mock
|
||||
grafanarest.LegacyStorage
|
||||
}
|
||||
|
||||
type storageMock struct {
|
||||
*mock.Mock
|
||||
grafanarest.Storage
|
||||
}
|
||||
|
||||
func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, name, options)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
|
||||
func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, obj, createValidation, options)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
|
||||
func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, options)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
|
||||
func (m legacyStoreMock) NewList() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, false, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
if err := args.Get(2); err != nil {
|
||||
return nil, false, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, false, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, name, deleteValidation, options)
|
||||
if err := args.Get(2); err != nil {
|
||||
return nil, false, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
args := m.Called(ctx, deleteValidation, options, listOptions)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
|
||||
// Unified Store
|
||||
func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, name, options)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
|
||||
func (m storageMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, obj, createValidation, options)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
|
||||
func (m storageMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, options)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
|
||||
func (m storageMock) NewList() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m storageMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, false, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
if err := args.Get(2); err != nil {
|
||||
return nil, false, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m storageMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, false, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, name, deleteValidation, options)
|
||||
if err := args.Get(2); err != nil {
|
||||
return nil, false, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
func (m storageMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("context canceled")
|
||||
default:
|
||||
}
|
||||
|
||||
args := m.Called(ctx, deleteValidation, options, listOptions)
|
||||
if err := args.Get(1); err != nil {
|
||||
return nil, err.(error)
|
||||
}
|
||||
return args.Get(0).(runtime.Object), args.Error(1)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
)
|
||||
|
||||
// For *legacy* services, this will indicate if we have transitioned to Unified storage yet
|
||||
type StorageStatus struct {
|
||||
Group string `json:"group" xorm:"group"`
|
||||
Resource string `json:"resource" xorm:"resource"`
|
||||
WriteLegacy bool `json:"write_legacy" xorm:"write_legacy"`
|
||||
WriteUnified bool `json:"write_unified" xorm:"write_unified"`
|
||||
|
||||
// Unified is the primary source (legacy may be secondary)
|
||||
ReadUnified bool `json:"read_unified" xorm:"read_unified"`
|
||||
|
||||
// Timestamp when a migration finished
|
||||
Migrated int64 `json:"migrated" xorm:"migrated"`
|
||||
|
||||
// Timestamp when a migration *started* this should be cleared when finished
|
||||
// While migrating all write commands will be unavailable
|
||||
Migrating int64 `json:"migrating" xorm:"migrating"`
|
||||
|
||||
// When false, the behavior will not change at runtime
|
||||
Runtime bool `json:"runtime" xorm:"runtime"`
|
||||
|
||||
// UpdateKey used for optimistic locking -- requests to change the status must match previous value
|
||||
UpdateKey int64 `json:"update_key" xorm:"update_key"`
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
ShouldManage(gr schema.GroupResource) bool
|
||||
|
||||
// Create a managed k8s storage instance
|
||||
NewStorage(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error)
|
||||
|
||||
// Check if the dual writes is reading from unified storage (mode3++)
|
||||
ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error)
|
||||
|
||||
// Get status details for a Group/Resource
|
||||
Status(ctx context.Context, gr schema.GroupResource) (StorageStatus, error)
|
||||
|
||||
// Start a migration process (writes will be locked)
|
||||
StartMigration(ctx context.Context, gr schema.GroupResource, key int64) (StorageStatus, error)
|
||||
|
||||
// change the status (finish migration etc)
|
||||
Update(ctx context.Context, status StorageStatus) (StorageStatus, error)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
)
|
||||
|
||||
func IsReadingLegacyDashboardsAndFolders(ctx context.Context, svc Service) bool {
|
||||
f, _ := svc.ReadFromUnified(ctx, folders.FolderResourceInfo.GroupResource())
|
||||
d, _ := svc.ReadFromUnified(ctx, schema.GroupResource{
|
||||
Group: dashboard.GROUP,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
})
|
||||
return !(f && d)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/federated"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
@@ -53,7 +54,7 @@ func TestDirectSQLStats(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db),
|
||||
nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, sort.ProvideService())
|
||||
nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
// create parent folder
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.266.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
|
||||
@@ -1,20 +1,58 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceClient, legacyClient ResourceIndexClient) ResourceIndexClient {
|
||||
config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey]
|
||||
if !ok {
|
||||
return legacyClient
|
||||
func NewSearchClient(dual dualwrite.Service, gr schema.GroupResource, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient {
|
||||
status, _ := dual.Status(context.Background(), gr)
|
||||
if status.Runtime && dual.ShouldManage(gr) {
|
||||
return &searchWrapper{
|
||||
dual: dual,
|
||||
groupResource: gr,
|
||||
unifiedClient: unifiedClient,
|
||||
legacyClient: legacyClient,
|
||||
}
|
||||
}
|
||||
|
||||
switch config.DualWriterMode {
|
||||
case rest.Mode0, rest.Mode1, rest.Mode2:
|
||||
return legacyClient
|
||||
default:
|
||||
if status.ReadUnified {
|
||||
return unifiedClient
|
||||
}
|
||||
return legacyClient
|
||||
}
|
||||
|
||||
type searchWrapper struct {
|
||||
dual dualwrite.Service
|
||||
groupResource schema.GroupResource
|
||||
|
||||
unifiedClient ResourceIndexClient
|
||||
legacyClient ResourceIndexClient
|
||||
}
|
||||
|
||||
func (s *searchWrapper) GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error) {
|
||||
client := s.legacyClient
|
||||
unified, err := s.dual.ReadFromUnified(ctx, s.groupResource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unified {
|
||||
client = s.unifiedClient
|
||||
}
|
||||
return client.GetStats(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (s *searchWrapper) Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error) {
|
||||
client := s.legacyClient
|
||||
unified, err := s.dual.ReadFromUnified(ctx, s.groupResource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if unified {
|
||||
client = s.unifiedClient
|
||||
}
|
||||
return client.Search(ctx, in, opts...)
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
"tags": [
|
||||
"Dashboard"
|
||||
],
|
||||
"description": "list or watch objects of kind Dashboard",
|
||||
"description": "list objects of kind Dashboard",
|
||||
"operationId": "listDashboard",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2501,32 +2501,6 @@
|
||||
"description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent": {
|
||||
"description": "Event represents a single event to a watched resource.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"object"
|
||||
],
|
||||
"properties": {
|
||||
"object": {
|
||||
"description": "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension"
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.apimachinery.pkg.runtime.RawExtension": {
|
||||
"description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// External package:\n\n\ttype MyAPIObject struct {\n\t\truntime.TypeMeta `json:\",inline\"`\n\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n\t}\n\n\ttype PluginA struct {\n\t\tAOption string `json:\"aOption\"`\n\t}\n\n// On the wire, the JSON will look something like this:\n\n\t{\n\t\t\"kind\":\"MyAPIObject\",\n\t\t\"apiVersion\":\"v1\",\n\t\t\"myPlugin\": {\n\t\t\t\"kind\":\"PluginA\",\n\t\t\t\"aOption\":\"foo\",\n\t\t},\n\t}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user