QueryService: Add feature toggles to better support testing (#86493)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# Query service
|
||||
|
||||
This query service aims to replace the existing /api/ds/query.
|
||||
|
||||
The key differences are:
|
||||
1. This service has a stronger type system (not simplejson)
|
||||
2. Same workflow regardless if expressions exist
|
||||
3. Datasource settings+access is managed in each datasource, not at the beginning
|
||||
|
||||
|
||||
|
||||
### Current /api/ds/query workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor User as User or Process
|
||||
participant api as /api/ds/query
|
||||
participant db as Storage<br/>(SQL)
|
||||
participant ds as Datasource<br/>Plugin
|
||||
participant expr as Expression<br/>Engine
|
||||
|
||||
User->>api: POST Query
|
||||
loop Each query
|
||||
api->>api: Parse query
|
||||
api->>db: Get ds config<br>and secrets
|
||||
db->>api:
|
||||
end
|
||||
alt No expressions
|
||||
alt Single datasource
|
||||
api->>ds: QueryData
|
||||
else Multiple datasources
|
||||
loop Each datasource (concurrently)
|
||||
api->>ds: QueryData
|
||||
end
|
||||
api->>api: Wait for results
|
||||
end
|
||||
else Expressions exist
|
||||
api->>expr: Calculate expressions graph
|
||||
loop Each node (eg, refID)
|
||||
alt Is query
|
||||
expr->>ds: QueryData
|
||||
else Is expression
|
||||
expr->>expr: Process
|
||||
end
|
||||
end
|
||||
end
|
||||
api->>User: return results
|
||||
```
|
||||
|
||||
|
||||
|
||||
### /apis/query.grafana.app (in single tenant grafana)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor User as User or Process
|
||||
participant api as /apis/query.grafana.app
|
||||
participant ds as Datasource<br/>Handler/Plugin
|
||||
participant db as Storage<br/>(SQL)
|
||||
participant expr as Expression<br/>Engine
|
||||
|
||||
User->>api: POST Query
|
||||
api->>api: Parse queries
|
||||
api->>api: Calculate dependencies
|
||||
loop Each datasource (concurrently)
|
||||
api->>ds: QueryData
|
||||
ds->>ds: Verify user access
|
||||
ds->>db: Get settings <br> and secrets
|
||||
end
|
||||
loop Each expression
|
||||
api->>expr: Execute
|
||||
end
|
||||
api->>api: Verify ResultExpectations
|
||||
api->>User: return results
|
||||
```
|
||||
|
||||
@@ -40,7 +40,7 @@ type pluginRegistry struct {
|
||||
var _ data.QueryDataClient = (*pluginClient)(nil)
|
||||
var _ query.DataSourceApiServerRegistry = (*pluginRegistry)(nil)
|
||||
|
||||
// NewDummyTestRunner creates a runner that only works with testdata
|
||||
// NewQueryClientForPluginClient creates a client that delegates to the internal plugins.Client stack
|
||||
func NewQueryClientForPluginClient(p plugins.Client, ctx *plugincontext.Provider) data.QueryDataClient {
|
||||
return &pluginClient{
|
||||
pluginClient: p,
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
example "github.com/grafana/grafana/pkg/apis/example/v0alpha1"
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
)
|
||||
|
||||
@@ -24,6 +23,9 @@ type pluginsStorage struct {
|
||||
resourceInfo *common.ResourceInfo
|
||||
tableConverter rest.TableConvertor
|
||||
registry query.DataSourceApiServerRegistry
|
||||
|
||||
// Always return an empty list regardless what we think exists
|
||||
returnEmptyList bool
|
||||
}
|
||||
|
||||
func newPluginsStorage(reg query.DataSourceApiServerRegistry) *pluginsStorage {
|
||||
@@ -46,7 +48,7 @@ func (s *pluginsStorage) NamespaceScoped() bool {
|
||||
}
|
||||
|
||||
func (s *pluginsStorage) GetSingularName() string {
|
||||
return example.DummyResourceInfo.GetSingularName()
|
||||
return s.resourceInfo.GetSingularName()
|
||||
}
|
||||
|
||||
func (s *pluginsStorage) NewList() runtime.Object {
|
||||
@@ -58,5 +60,8 @@ func (s *pluginsStorage) ConvertToTable(ctx context.Context, object runtime.Obje
|
||||
}
|
||||
|
||||
func (s *pluginsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
if s.returnEmptyList {
|
||||
return s.NewList(), nil
|
||||
}
|
||||
return s.registry.GetDatasourceApiServers(ctx)
|
||||
}
|
||||
|
||||
@@ -85,8 +85,9 @@ func RegisterAPIService(features featuremgmt.FeatureToggles,
|
||||
tracer tracing.Tracer,
|
||||
legacy service.LegacyDataSourceLookup,
|
||||
) (*QueryAPIBuilder, error) {
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil, nil // skip registration unless opting into experimental apis
|
||||
if !(features.IsEnabledGlobally(featuremgmt.FlagQueryService) ||
|
||||
features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)) {
|
||||
return nil, nil // skip registration unless explicitly added (or all experimental are added)
|
||||
}
|
||||
|
||||
builder, err := NewQueryAPIBuilder(
|
||||
@@ -132,10 +133,16 @@ func (b *QueryAPIBuilder) GetAPIGroupInfo(
|
||||
gv := v0alpha1.SchemeGroupVersion
|
||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(gv.Group, scheme, metav1.ParameterCodec, codecs)
|
||||
|
||||
plugins := newPluginsStorage(b.registry)
|
||||
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
plugins := newPluginsStorage(b.registry)
|
||||
storage[plugins.resourceInfo.StoragePath()] = plugins
|
||||
if !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
// The plugin registry is still experimental, and not yet accurate
|
||||
// For standard k8s api discovery to work, at least one resource must be registered
|
||||
// While this feature is under development, we can return an empty list for non-dev instances
|
||||
plugins.returnEmptyList = true
|
||||
}
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[gv.Version] = storage
|
||||
return &apiGroupInfo, nil
|
||||
|
||||
Reference in New Issue
Block a user