mirror of
https://github.com/grafana/grafana.git
synced 2026-06-11 09:31:50 -04:00
The built-in "-- Grafana --" datasource has no row in the datasource table, so legacyConnectionClientImpl.GetConnectionByUID could never resolve its plugin type. For RBAC roles carrying the seeded datasources:uid:grafana grant this was silently unresolved, and for global roles (orgID 0, multi-org) the store lookup failed with ErrDataSourceIdentifierNotSet, breaking role provisioning. Its UID and type are fixed and identical across orgs, so resolve it directly in the legacy connection client. This fixes datasource-type resolution for every caller that goes through this client (provisioning save and the k8s migration type resolvers) without a store row. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
5.9 KiB
Go
161 lines
5.9 KiB
Go
package datasource
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/client-go/dynamic"
|
|
"k8s.io/client-go/rest"
|
|
|
|
dsV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
|
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
// TODO: move this to /apis/query/v0alpha1 once DirectRestConfigProvider has been
|
|
// moved to its own package (currently we get an import cycle)
|
|
//
|
|
// ConnectionClient provides access to datasource connection info via K8s API.
|
|
// Methods that don't take a plugin type argument are deprecated and will be removed.
|
|
//
|
|
// This client is useful for services that currently do not know about a datasource's type when fetching it.
|
|
type ConnectionClient interface {
|
|
// GetConnectionByUID looks up a datasource connection by UID.
|
|
// Returns one or more connections which contains the API group (plugin type).
|
|
// If more that one connection is returned, it should be considered an error
|
|
// as we cannot guarantee which resource the caller intended to get.
|
|
//
|
|
// Deprecated: Use /apis/<type>.datasource.grafana.app/v0alpha1/namespaces/{ns}/datasources/{uid} instead.
|
|
GetConnectionByUID(ctx context.Context, orgID int64, uid string) (*dsV0.DataSourceConnectionList, error)
|
|
}
|
|
|
|
var _ ConnectionClient = (*connectionClientImpl)(nil)
|
|
|
|
// connectionClientImpl implements the ConnectionClient interface.
|
|
type connectionClientImpl struct {
|
|
clientConfigProvider grafanaapiserver.RestConfigProvider
|
|
namespaceMapper request.NamespaceMapper
|
|
}
|
|
|
|
// NewConnectionClient creates a new ConnectionClient that queries the connections endpoint in the query api group.
|
|
func NewConnectionClient(cfg *setting.Cfg, provider grafanaapiserver.RestConfigProvider) ConnectionClient {
|
|
return &connectionClientImpl{
|
|
clientConfigProvider: provider,
|
|
namespaceMapper: request.GetNamespaceMapper(cfg),
|
|
}
|
|
}
|
|
|
|
// GetConnectionByUID queries GET /apis/datasource.grafana.app/v0alpha1/namespaces/{ns}/connections/{uid}
|
|
// Deprecated: Use GetConnectionByTypeAndUID when type is known.
|
|
func (cl *connectionClientImpl) GetConnectionByUID(ctx context.Context, orgID int64, uid string) (*dsV0.DataSourceConnectionList, error) {
|
|
namespace := cl.namespaceMapper(orgID)
|
|
|
|
restCfg, err := cl.clientConfigProvider.GetRestConfig(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get rest config: %w", err)
|
|
}
|
|
|
|
cfg := dynamic.ConfigFor(restCfg) // This sets NegotiatedSerializer, required for RESTClientFor
|
|
cfg.GroupVersion = &dsV0.SchemeGroupVersion
|
|
client, err := rest.RESTClientFor(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create rest client: %w", err)
|
|
}
|
|
|
|
var statusCode int
|
|
result := client.Get().AbsPath("apis", dsV0.GROUP, dsV0.VERSION, "namespaces", namespace, "connections").Param("name", uid).Do(ctx).StatusCode(&statusCode)
|
|
err = result.Error()
|
|
if err != nil {
|
|
if errors.IsNotFound(err) {
|
|
return nil, fmt.Errorf("datasource connection not found: %s", uid)
|
|
}
|
|
return nil, fmt.Errorf("failed to get connection: %w", err)
|
|
}
|
|
|
|
body, _ := result.Raw() // err has already been checked
|
|
|
|
var conn dsV0.DataSourceConnectionList
|
|
if err := json.Unmarshal(body, &conn); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal connection: %w", err)
|
|
}
|
|
|
|
return &conn, nil
|
|
}
|
|
|
|
// legacyConnectionClientImpl implements ConnectionClient
|
|
//
|
|
// This client is a temporary implementation so we reroute datasource CRUD requests
|
|
// without relying on the query.grafana.app API group. We're using the legacy
|
|
// datasource service just to get the datasource type, then forwarding the request
|
|
// to the new APIs.
|
|
type legacyConnectionClientImpl struct {
|
|
datasourceService datasourceservice.DataSourceRetriever
|
|
}
|
|
|
|
var _ ConnectionClient = (*legacyConnectionClientImpl)(nil)
|
|
|
|
// builtinGrafanaDatasourceUID and builtinGrafanaDatasourceType identify the built-in
|
|
// "-- Grafana --" datasource. They mirror grafanads.DatasourceUID / its plugin type, kept
|
|
// local here to avoid importing the tsdb package into this central API package.
|
|
const (
|
|
builtinGrafanaDatasourceUID = "grafana"
|
|
builtinGrafanaDatasourceType = "grafana"
|
|
)
|
|
|
|
// NewLegacyConnectionClient creates a new ConnectionClient that relies on the legacy datasource service.
|
|
func NewLegacyConnectionClient(datasourceService datasourceservice.DataSourceRetriever) ConnectionClient {
|
|
return &legacyConnectionClientImpl{
|
|
datasourceService: datasourceService,
|
|
}
|
|
}
|
|
|
|
func (cl *legacyConnectionClientImpl) GetConnectionByUID(ctx context.Context, orgID int64, uid string) (*dsV0.DataSourceConnectionList, error) {
|
|
// The built-in "-- Grafana --" datasource has no row in the datasource table, so a store
|
|
// lookup can never resolve its type and fails outright when no org is set (e.g. global RBAC
|
|
// roles, which carry a seeded datasources:uid:grafana grant). Its UID and type are fixed and
|
|
// identical across orgs, so resolve it directly.
|
|
if uid == builtinGrafanaDatasourceUID {
|
|
return &dsV0.DataSourceConnectionList{
|
|
Items: []dsV0.DataSourceConnection{
|
|
{
|
|
APIGroup: builtinGrafanaDatasourceType + ".datasource.grafana.app",
|
|
APIVersion: "v0alpha1",
|
|
Name: uid,
|
|
Plugin: builtinGrafanaDatasourceType,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
query := datasources.GetDataSourceQuery{
|
|
UID: uid,
|
|
OrgID: orgID,
|
|
}
|
|
|
|
conn, err := cl.datasourceService.GetDataSource(ctx, &query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if conn == nil {
|
|
return &dsV0.DataSourceConnectionList{
|
|
Items: []dsV0.DataSourceConnection{},
|
|
}, nil
|
|
}
|
|
|
|
return &dsV0.DataSourceConnectionList{
|
|
Items: []dsV0.DataSourceConnection{
|
|
{
|
|
APIGroup: conn.Type + ".datasource.grafana.app",
|
|
APIVersion: "v0alpha1",
|
|
Name: conn.UID,
|
|
Plugin: conn.Type,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|