2024-01-10 04:26:24 +08:00
|
|
|
package datasource
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-05-24 00:46:28 +08:00
|
|
|
"encoding/json"
|
2025-09-04 03:48:24 +08:00
|
|
|
"errors"
|
2025-08-29 20:49:57 +08:00
|
|
|
"fmt"
|
2025-08-29 22:46:39 +08:00
|
|
|
"maps"
|
2025-09-04 03:48:24 +08:00
|
|
|
"path/filepath"
|
2024-01-10 04:26:24 +08:00
|
|
|
|
2024-10-15 12:46:08 +08:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"k8s.io/apiserver/pkg/registry/rest"
|
|
|
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
|
|
|
openapi "k8s.io/kube-openapi/pkg/common"
|
|
|
|
"k8s.io/utils/strings/slices"
|
|
|
|
|
2024-03-09 00:12:59 +08:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
2024-09-04 19:53:14 +08:00
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
2025-08-29 22:46:39 +08:00
|
|
|
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
|
|
|
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
|
|
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
2025-10-03 07:53:31 +08:00
|
|
|
"github.com/grafana/grafana/pkg/configprovider"
|
2024-08-29 23:06:25 +08:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2024-09-04 19:53:14 +08:00
|
|
|
"github.com/grafana/grafana/pkg/plugins"
|
2025-09-04 03:48:24 +08:00
|
|
|
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
2024-09-04 19:53:14 +08:00
|
|
|
"github.com/grafana/grafana/pkg/promlib/models"
|
|
|
|
"github.com/grafana/grafana/pkg/registry/apis/query/queryschema"
|
|
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
|
|
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
2025-09-04 03:48:24 +08:00
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
2024-09-04 19:53:14 +08:00
|
|
|
"github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource/kinds"
|
2024-01-10 04:26:24 +08:00
|
|
|
)
|
|
|
|
|
2025-08-29 22:46:39 +08:00
|
|
|
var (
|
|
|
|
_ builder.APIGroupBuilder = (*DataSourceAPIBuilder)(nil)
|
|
|
|
)
|
2024-01-10 04:26:24 +08:00
|
|
|
|
2024-01-19 22:56:52 +08:00
|
|
|
// DataSourceAPIBuilder is used just so wire has something unique to return
|
2024-01-10 04:26:24 +08:00
|
|
|
type DataSourceAPIBuilder struct {
|
2025-08-29 22:46:39 +08:00
|
|
|
datasourceResourceInfo utils.ResourceInfo
|
2024-01-10 04:26:24 +08:00
|
|
|
|
2025-09-04 03:48:24 +08:00
|
|
|
pluginJSON plugins.JSONData
|
|
|
|
client PluginClient // will only ever be called with the same plugin id!
|
|
|
|
datasources PluginDatasourceProvider
|
|
|
|
contextProvider PluginContextWrapper
|
|
|
|
accessControl accesscontrol.AccessControl
|
|
|
|
queryTypes *queryV0.QueryTypeDefinitionList
|
|
|
|
log log.Logger
|
|
|
|
configCrudUseNewApis bool
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func RegisterAPIService(
|
2025-10-03 07:53:31 +08:00
|
|
|
cfgProvider configprovider.ConfigProvider,
|
2024-01-10 04:26:24 +08:00
|
|
|
features featuremgmt.FeatureToggles,
|
2024-02-02 06:27:30 +08:00
|
|
|
apiRegistrar builder.APIRegistrar,
|
2024-01-23 03:32:25 +08:00
|
|
|
pluginClient plugins.Client, // access to everything
|
2024-03-25 22:22:34 +08:00
|
|
|
datasources ScopedPluginDatasourceProvider,
|
2024-01-23 03:32:25 +08:00
|
|
|
contextProvider PluginContextWrapper,
|
2024-01-10 04:26:24 +08:00
|
|
|
accessControl accesscontrol.AccessControl,
|
2024-06-14 17:01:49 +08:00
|
|
|
reg prometheus.Registerer,
|
2024-01-10 04:26:24 +08:00
|
|
|
) (*DataSourceAPIBuilder, error) {
|
2024-09-19 16:28:27 +08:00
|
|
|
// We want to expose just a limited set of plugins
|
|
|
|
explictPluginList := features.IsEnabledGlobally(featuremgmt.FlagDatasourceAPIServers)
|
|
|
|
|
2024-01-10 04:26:24 +08:00
|
|
|
// This requires devmode!
|
2025-04-10 20:42:23 +08:00
|
|
|
if !explictPluginList && !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
2024-01-10 04:26:24 +08:00
|
|
|
return nil, nil // skip registration unless opting into experimental apis
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
var builder *DataSourceAPIBuilder
|
2025-10-03 07:53:31 +08:00
|
|
|
|
|
|
|
cfg, err := cfgProvider.Get(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pluginJSONs, err := getCorePlugins(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-01-10 04:26:24 +08:00
|
|
|
ids := []string{
|
|
|
|
"grafana-testdata-datasource",
|
2024-03-25 22:22:34 +08:00
|
|
|
"prometheus",
|
|
|
|
"graphite",
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
|
|
|
|
2025-10-03 07:53:31 +08:00
|
|
|
for _, pluginJSON := range pluginJSONs {
|
|
|
|
if explictPluginList && !slices.Contains(ids, pluginJSON.ID) {
|
2024-01-10 04:26:24 +08:00
|
|
|
continue // skip this one
|
|
|
|
}
|
|
|
|
|
2025-10-03 07:53:31 +08:00
|
|
|
if !pluginJSON.Backend {
|
2024-09-24 18:39:11 +08:00
|
|
|
continue // skip frontend only plugins
|
|
|
|
}
|
|
|
|
|
2025-10-03 07:53:31 +08:00
|
|
|
if pluginJSON.Type != plugins.TypeDataSource {
|
|
|
|
continue // skip non-datasource plugins
|
|
|
|
}
|
|
|
|
|
|
|
|
client, ok := pluginClient.(PluginClient)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("plugin client is not a PluginClient: %T", pluginClient)
|
|
|
|
}
|
|
|
|
|
|
|
|
builder, err = NewDataSourceAPIBuilder(pluginJSON,
|
|
|
|
client,
|
|
|
|
datasources.GetDatasourceProvider(pluginJSON),
|
2024-01-23 03:32:25 +08:00
|
|
|
contextProvider,
|
|
|
|
accessControl,
|
2024-05-24 00:46:28 +08:00
|
|
|
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
|
2025-09-04 03:48:24 +08:00
|
|
|
false,
|
2024-01-23 03:32:25 +08:00
|
|
|
)
|
2024-01-10 04:26:24 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2025-08-29 22:46:39 +08:00
|
|
|
|
2024-01-19 22:56:52 +08:00
|
|
|
apiRegistrar.RegisterAPI(builder)
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
|
|
|
return builder, nil // only used for wire
|
|
|
|
}
|
|
|
|
|
2024-02-26 20:02:55 +08:00
|
|
|
// PluginClient is a subset of the plugins.Client interface with only the
|
|
|
|
// functions supported (yet) by the datasource API
|
|
|
|
type PluginClient interface {
|
|
|
|
backend.QueryDataHandler
|
|
|
|
backend.CheckHealthHandler
|
|
|
|
backend.CallResourceHandler
|
2024-09-25 21:10:19 +08:00
|
|
|
backend.ConversionHandler
|
2024-02-26 20:02:55 +08:00
|
|
|
}
|
|
|
|
|
2024-01-10 04:26:24 +08:00
|
|
|
func NewDataSourceAPIBuilder(
|
2024-01-10 23:45:23 +08:00
|
|
|
plugin plugins.JSONData,
|
2024-02-26 20:02:55 +08:00
|
|
|
client PluginClient,
|
2024-01-24 23:44:40 +08:00
|
|
|
datasources PluginDatasourceProvider,
|
2024-01-23 03:32:25 +08:00
|
|
|
contextProvider PluginContextWrapper,
|
2024-05-24 00:46:28 +08:00
|
|
|
accessControl accesscontrol.AccessControl,
|
|
|
|
loadQueryTypes bool,
|
2025-09-04 03:48:24 +08:00
|
|
|
configCrudUseNewApis bool,
|
2024-05-24 00:46:28 +08:00
|
|
|
) (*DataSourceAPIBuilder, error) {
|
2025-08-29 22:46:39 +08:00
|
|
|
group, err := plugins.GetDatasourceGroupNameFromPluginID(plugin.ID)
|
2024-01-10 04:26:24 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-01-19 22:56:52 +08:00
|
|
|
|
2024-05-24 00:46:28 +08:00
|
|
|
builder := &DataSourceAPIBuilder{
|
2025-08-29 22:46:39 +08:00
|
|
|
datasourceResourceInfo: datasourceV0.DataSourceResourceInfo.WithGroupAndShortName(group, plugin.ID),
|
2024-01-19 22:56:52 +08:00
|
|
|
pluginJSON: plugin,
|
2024-01-23 03:32:25 +08:00
|
|
|
client: client,
|
2024-01-24 23:44:40 +08:00
|
|
|
datasources: datasources,
|
2024-01-23 03:32:25 +08:00
|
|
|
contextProvider: contextProvider,
|
2024-01-10 04:26:24 +08:00
|
|
|
accessControl: accessControl,
|
2024-08-29 23:06:25 +08:00
|
|
|
log: log.New("grafana-apiserver.datasource"),
|
2025-09-04 03:48:24 +08:00
|
|
|
configCrudUseNewApis: configCrudUseNewApis,
|
2024-05-24 00:46:28 +08:00
|
|
|
}
|
|
|
|
if loadQueryTypes {
|
|
|
|
// In the future, this will somehow come from the plugin
|
2025-08-29 22:46:39 +08:00
|
|
|
builder.queryTypes, err = getHardcodedQueryTypes(group)
|
2024-05-24 00:46:28 +08:00
|
|
|
}
|
|
|
|
return builder, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO -- somehow get the list from the plugin -- not hardcoded
|
2025-08-29 22:46:39 +08:00
|
|
|
func getHardcodedQueryTypes(group string) (*queryV0.QueryTypeDefinitionList, error) {
|
2024-05-24 00:46:28 +08:00
|
|
|
var err error
|
|
|
|
var raw json.RawMessage
|
|
|
|
switch group {
|
|
|
|
case "testdata.datasource.grafana.app":
|
|
|
|
raw, err = kinds.QueryTypeDefinitionListJSON()
|
|
|
|
case "prometheus.datasource.grafana.app":
|
|
|
|
raw, err = models.QueryTypeDefinitionListJSON()
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if raw != nil {
|
2025-08-29 22:46:39 +08:00
|
|
|
types := &queryV0.QueryTypeDefinitionList{}
|
2024-05-24 00:46:28 +08:00
|
|
|
err = json.Unmarshal(raw, types)
|
|
|
|
return types, err
|
|
|
|
}
|
|
|
|
return nil, err
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func (b *DataSourceAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
2025-08-29 22:46:39 +08:00
|
|
|
return b.datasourceResourceInfo.GroupVersion()
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
|
|
|
scheme.AddKnownTypes(gv,
|
2025-08-29 22:46:39 +08:00
|
|
|
&datasourceV0.DataSource{},
|
|
|
|
&datasourceV0.DataSourceList{},
|
|
|
|
&datasourceV0.HealthCheckResult{},
|
2024-01-10 04:26:24 +08:00
|
|
|
&unstructured.Unstructured{},
|
2025-08-29 22:46:39 +08:00
|
|
|
|
2024-02-01 02:36:51 +08:00
|
|
|
// Query handler
|
2025-08-29 22:46:39 +08:00
|
|
|
&queryV0.QueryDataRequest{},
|
|
|
|
&queryV0.QueryDataResponse{},
|
|
|
|
&queryV0.QueryTypeDefinition{},
|
|
|
|
&queryV0.QueryTypeDefinitionList{},
|
2024-01-10 04:26:24 +08:00
|
|
|
&metav1.Status{},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *DataSourceAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
2025-08-29 22:46:39 +08:00
|
|
|
gv := b.datasourceResourceInfo.GroupVersion()
|
2024-01-10 04:26:24 +08:00
|
|
|
addKnownTypes(scheme, gv)
|
|
|
|
|
|
|
|
// Link this version to the internal representation.
|
|
|
|
// This is used for server-side-apply (PATCH), and avoids the error:
|
|
|
|
// "no kind is registered for the type"
|
|
|
|
addKnownTypes(scheme, schema.GroupVersion{
|
|
|
|
Group: gv.Group,
|
|
|
|
Version: runtime.APIVersionInternal,
|
|
|
|
})
|
|
|
|
|
|
|
|
// If multiple versions exist, then register conversions from zz_generated.conversion.go
|
|
|
|
// if err := playlist.RegisterConversions(scheme); err != nil {
|
|
|
|
// return err
|
|
|
|
// }
|
|
|
|
metav1.AddToGroupVersion(scheme, gv)
|
|
|
|
return scheme.SetVersionPriority(gv)
|
|
|
|
}
|
|
|
|
|
2025-06-21 05:37:17 +08:00
|
|
|
func (b *DataSourceAPIBuilder) AllowedV0Alpha1Resources() []string {
|
|
|
|
return []string{builder.AllResourcesAllowed}
|
|
|
|
}
|
|
|
|
|
2025-08-29 22:46:39 +08:00
|
|
|
func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
2025-08-29 20:49:57 +08:00
|
|
|
storage := map[string]rest.Storage{}
|
|
|
|
|
2025-08-29 22:46:39 +08:00
|
|
|
// Register the raw datasource connection
|
|
|
|
ds := b.datasourceResourceInfo
|
|
|
|
storage[ds.StoragePath("query")] = &subQueryREST{builder: b}
|
|
|
|
storage[ds.StoragePath("health")] = &subHealthREST{builder: b}
|
|
|
|
storage[ds.StoragePath("resource")] = &subResourceREST{builder: b}
|
|
|
|
|
|
|
|
// FIXME: temporarily register both "datasources" and "connections" query paths
|
|
|
|
// This lets us deploy both datasources/{uid}/query and connections/{uid}/query
|
|
|
|
// while we transition requests to the new path
|
|
|
|
storage["connections"] = &noopREST{} // hidden from openapi
|
|
|
|
storage["connections/query"] = storage[ds.StoragePath("query")] // deprecated in openapi
|
2024-01-10 04:26:24 +08:00
|
|
|
|
2025-09-04 03:48:24 +08:00
|
|
|
if b.configCrudUseNewApis {
|
|
|
|
legacyStore := &legacyStorage{
|
|
|
|
datasources: b.datasources,
|
|
|
|
resourceInfo: &ds,
|
|
|
|
}
|
|
|
|
unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
storage[ds.StoragePath()], err = opts.DualWriteBuilder(ds.GroupResource(), legacyStore, unified)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
storage[ds.StoragePath()] = &connectionAccess{
|
|
|
|
datasources: b.datasources,
|
|
|
|
resourceInfo: ds,
|
|
|
|
tableConverter: ds.TableConverter(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-10 04:26:24 +08:00
|
|
|
// Frontend proxy
|
2024-01-19 22:56:52 +08:00
|
|
|
if len(b.pluginJSON.Routes) > 0 {
|
2025-08-29 22:46:39 +08:00
|
|
|
storage[ds.StoragePath("proxy")] = &subProxyREST{pluginJSON: b.pluginJSON}
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
|
|
|
|
2024-05-24 00:46:28 +08:00
|
|
|
// Register hardcoded query schemas
|
2025-09-04 03:48:24 +08:00
|
|
|
err := queryschema.RegisterQueryTypes(b.queryTypes, storage)
|
2024-09-25 21:10:19 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
registerQueryConvert(b.client, b.contextProvider, storage)
|
2024-05-24 00:46:28 +08:00
|
|
|
|
2025-08-29 22:46:39 +08:00
|
|
|
apiGroupInfo.VersionedResourcesStorageMap[ds.GroupVersion().Version] = storage
|
2024-09-24 10:07:52 +08:00
|
|
|
return err
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
|
|
|
|
2024-01-23 03:32:25 +08:00
|
|
|
func (b *DataSourceAPIBuilder) getPluginContext(ctx context.Context, uid string) (backend.PluginContext, error) {
|
2024-03-20 20:49:19 +08:00
|
|
|
instance, err := b.datasources.GetInstanceSettings(ctx, uid)
|
2024-01-23 03:32:25 +08:00
|
|
|
if err != nil {
|
|
|
|
return backend.PluginContext{}, err
|
|
|
|
}
|
|
|
|
return b.contextProvider.PluginContextForDataSource(ctx, instance)
|
|
|
|
}
|
|
|
|
|
2024-01-13 04:05:30 +08:00
|
|
|
func (b *DataSourceAPIBuilder) GetOpenAPIDefinitions() openapi.GetOpenAPIDefinitions {
|
2024-02-02 14:06:28 +08:00
|
|
|
return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition {
|
2025-08-29 22:46:39 +08:00
|
|
|
defs := queryV0.GetOpenAPIDefinitions(ref) // required when running standalone
|
|
|
|
maps.Copy(defs, datasourceV0.GetOpenAPIDefinitions(ref))
|
2024-02-02 14:06:28 +08:00
|
|
|
return defs
|
|
|
|
}
|
2024-01-10 04:26:24 +08:00
|
|
|
}
|
2025-09-04 03:48:24 +08:00
|
|
|
|
|
|
|
func getCorePlugins(cfg *setting.Cfg) ([]plugins.JSONData, error) {
|
|
|
|
coreDataSourcesPath := filepath.Join(cfg.StaticRootPath, "app", "plugins", "datasource")
|
|
|
|
coreDataSourcesSrc := sources.NewLocalSource(
|
|
|
|
plugins.ClassCore,
|
|
|
|
[]string{coreDataSourcesPath},
|
|
|
|
)
|
|
|
|
|
|
|
|
res, err := coreDataSourcesSrc.Discover(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.New("failed to load core data source plugins")
|
|
|
|
}
|
|
|
|
|
|
|
|
pluginJSONs := make([]plugins.JSONData, 0, len(res))
|
|
|
|
for _, p := range res {
|
|
|
|
pluginJSONs = append(pluginJSONs, p.Primary.JSONData)
|
|
|
|
}
|
|
|
|
return pluginJSONs, nil
|
|
|
|
}
|