feat: provides MT Dashboard service (#110447)

This commit is contained in:
Costa Alexoglou
2025-09-03 20:41:37 +00:00
committed by GitHub
parent 7d32640179
commit 3d2cef5f07
43 changed files with 284 additions and 69 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/libraryelements"
)
func GetAuthorizer(ac accesscontrol.AccessControl, l log.Logger) authorizer.Authorizer {
func newLegacyAuthorizer(ac accesscontrol.AccessControl, l log.Logger) authorizer.Authorizer {
return authorizer.AuthorizerFunc(
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
// Note that we will return Allow more than expected.
+1 -1
View File
@@ -11,7 +11,7 @@ type datasourceInfoProvider struct {
datasourceService datasources.DataSourceService
}
func (d *datasourceInfoProvider) GetDataSourceInfo() []schemaversion.DataSourceInfo {
func (d *datasourceInfoProvider) GetDataSourceInfo(_ context.Context) []schemaversion.DataSourceInfo {
query := datasources.GetAllDataSourcesQuery{}
dataSources, err := d.datasourceService.GetAllDataSources(context.Background(), &query)
+1 -1
View File
@@ -53,7 +53,7 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
internalID = int64(id)
}
resourceInfo = dashboardV1.DashboardResourceInfo
migrationErr = migration.Migrate(v.Spec.Object, schemaversion.LATEST_VERSION)
migrationErr = migration.Migrate(ctx, v.Spec.Object, schemaversion.LATEST_VERSION)
if migrationErr != nil {
v.Status.Conversion = &dashboardV1.DashboardConversionStatus{
Failed: true,
+3 -3
View File
@@ -14,10 +14,10 @@ type PluginStorePanelProvider struct {
}
func (p *PluginStorePanelProvider) GetPanels() []schemaversion.PanelPluginInfo {
plugins := p.pluginStore.Plugins(context.Background(), plugins.TypePanel)
panelPlugins := p.pluginStore.Plugins(context.Background(), plugins.TypePanel)
panels := make([]schemaversion.PanelPluginInfo, len(plugins))
for i, plugin := range plugins {
panels := make([]schemaversion.PanelPluginInfo, len(panelPlugins))
for i, plugin := range panelPlugins {
version := plugin.Info.Version
if version == "" {
version = p.buildVersion
+81 -12
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"maps"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"github.com/prometheus/client_golang/prometheus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -20,7 +21,6 @@ import (
"k8s.io/kube-openapi/pkg/validation/spec"
claims "github.com/grafana/authlib/types"
internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
@@ -37,9 +37,11 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
"github.com/grafana/grafana/pkg/services/accesscontrol"
authsvc "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
@@ -77,6 +79,7 @@ type DashboardsAPIBuilder struct {
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
authorizer authorizer.Authorizer
accessControl accesscontrol.AccessControl
accessClient claims.AccessClient
legacy *DashboardStorage
@@ -92,10 +95,14 @@ type DashboardsAPIBuilder struct {
ProvisioningService provisioning.ProvisioningService
cfg *setting.Cfg
dualWriter dualwrite.Service
folderClient client.K8sHandler
log log.Logger
reg prometheus.Registerer
// only one of the two is required to be set, folderClientProvider is used when isStandalone is true
folderClient client.K8sHandler
folderClientProvider client.K8sHandlerProvider
log log.Logger
reg prometheus.Registerer
isStandalone bool // skips any handling including anything to do with legacy storage
}
func RegisterAPIService(
@@ -128,9 +135,11 @@ func RegisterAPIService(
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
folderClient := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), folders.FolderResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashStore, userService, unified, sorter, features)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards"),
dashLog := log.New("grafana-apiserver.dashboards")
builder := &DashboardsAPIBuilder{
log: dashLog,
authorizer: newLegacyAuthorizer(accessControl, dashLog),
dashboardService: dashboardService,
dashboardPermissions: dashboardPermissions,
dashboardPermissionsSvc: dashboardPermissionsSvc,
@@ -166,6 +175,37 @@ func RegisterAPIService(
return builder
}
func NewAPIService(ac claims.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceInfoProvider, pluginStore *pluginstore.Service) *DashboardsAPIBuilder {
// TODO: Plugin store will soon be removed,
// as the cases for plugin fetching is not needed. Keeping it now to not break implementation
if pluginStore == nil {
panic("pluginStore is nil")
}
logger := log.New("grafana-apiserver.dashboards")
migration.Initialize(datasourceProvider, &PluginStorePanelProvider{
pluginStore: pluginStore,
buildVersion: "unknown",
})
return &DashboardsAPIBuilder{
log: logger,
reg: prometheus.NewRegistry(),
cfg: &setting.Cfg{
MinRefreshInterval: "10s",
},
accessClient: ac,
authorizer: authsvc.NewResourceAuthorizer(ac),
features: features,
dashboardService: &dashsvc.DashboardServiceImpl{}, // for validation helpers only
folderClientProvider: folderClientProvider,
isStandalone: true,
}
}
func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion {
if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) {
// If dashboards v2 is enabled, we want to use v2beta1 as the default API version.
@@ -304,12 +344,12 @@ func (b *DashboardsAPIBuilder) validateCreate(ctx context.Context, a admission.A
// Validate folder existence if specified
if !a.IsDryRun() && accessor.GetFolder() != "" {
if err := b.validateFolderExists(ctx, accessor.GetFolder(), id.GetOrgID()); err != nil {
return apierrors.NewNotFound(folders.FolderResourceInfo.GroupResource(), accessor.GetFolder())
return err
}
}
// Validate quota
if !a.IsDryRun() {
if !b.isStandalone && !a.IsDryRun() {
params := &quota.ScopeParameters{}
params.OrgID = id.GetOrgID()
internalId, err := id.GetInternalID()
@@ -388,9 +428,24 @@ func (b *DashboardsAPIBuilder) validateUpdate(ctx context.Context, a admission.A
// validateFolderExists checks if a folder exists
func (b *DashboardsAPIBuilder) validateFolderExists(ctx context.Context, folderUID string, orgID int64) error {
// Check if folder exists using the folder store
_, err := b.folderClient.Get(ctx, folderUID, orgID, metav1.GetOptions{})
var folderClient client.K8sHandler
if b.isStandalone {
ns, err := request.NamespaceInfoFrom(ctx, false)
if err != nil {
return err
}
folderClient = b.folderClientProvider.GetOrCreateHandler(ns.Value)
} else {
folderClient = b.folderClient
}
_, err := folderClient.Get(ctx, folderUID, orgID, metav1.GetOptions{})
// Check if the error is a context deadline exceeded error
if err != nil {
// historically, we returned a more verbose error with folder name when its not found, below just keeps that behavior
if apierrors.IsNotFound(err) {
return apierrors.NewNotFound(folders.FolderResourceInfo.GroupResource(), folderUID)
}
return err
}
@@ -426,9 +481,13 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
storageOpts := apistore.StorageOptions{
EnableFolderSupport: true,
RequireDeprecatedInternalID: true,
}
if b.isStandalone {
// TODO: Sets default root permissions
} else {
// Sets default root permissions
Permissions: b.dashboardPermissions.SetDefaultPermissionsAfterCreate,
storageOpts.Permissions = b.dashboardPermissions.SetDefaultPermissionsAfterCreate
}
// Split dashboards when they are large
@@ -525,6 +584,16 @@ func (b *DashboardsAPIBuilder) storageForVersion(
storage := map[string]rest.Storage{}
apiGroupInfo.VersionedResourcesStorageMap[dashboards.GroupVersion().Version] = storage
if b.isStandalone {
store, err := grafanaregistry.NewRegistryStore(opts.Scheme, dashboards, opts.OptsGetter)
if err != nil {
return err
}
storage[dashboards.StoragePath()] = store
return nil
}
legacyStore, err := b.legacy.NewStore(dashboards, opts.Scheme, opts.OptsGetter, b.reg, b.dashboardPermissions, b.accessClient)
if err != nil {
return err
@@ -607,7 +676,7 @@ func (b *DashboardsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.API
}
func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return GetAuthorizer(b.accessControl, b.log)
return b.authorizer
}
func (b *DashboardsAPIBuilder) verifyFolderAccessPermissions(ctx context.Context, user identity.Requester, folderIds ...string) error {
+1 -1
View File
@@ -70,7 +70,7 @@ func authorizerFunc(ctx context.Context, attr authorizer.Attributes) (*authorize
return &authorizerParams{evaluator: eval, user: user}, nil
}
// newMultiTenantAuthorizer creates an authorizer sutiable to multi-tenant setup.
// newMultiTenantAuthorizer creates an authorizer suitable to multi-tenant setup.
// For now it only allow authorization of access tokens.
func newMultiTenantAuthorizer(ac types.AccessClient) authorizer.Authorizer {
return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
+6
View File
@@ -23,6 +23,12 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
// K8sHandlerProvider is a provider for K8sHandler instances.
// It is used to get a K8sHandler instance for a given namespace.
type K8sHandlerProvider interface {
GetOrCreateHandler(namespace string) K8sHandler
}
type K8sHandler interface {
GetNamespace(orgID int64) string
Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error)