Commit 38caa80a by Torkel Ödegaard Committed by GitHub

DataSourceSrv: Look up data source by uid and name transparently (#29449)

* Datasources: Look up data source by uid or name transparently

* comment tweak

* Removed config

* fixed type issues

* Initialize datasource srv

* Added deprecation notice

* Renamed getSettingsFor to getInstanceSettings

* fixed ts issue
parent b7aa6fed
......@@ -17,6 +17,7 @@ export interface DataSourceSrv {
/**
* Returns metadata based on UID.
* @deprecated use getInstanceSettings
*/
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined;
......@@ -29,6 +30,11 @@ export interface DataSourceSrv {
* Get all data sources except for internal ones that usually should not be listed like mixed data source.
*/
getExternal(): DataSourceInstanceSettings[];
/**
* Get settings and plugin metadata by name or uid
*/
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined;
}
let singletonInstance: DataSourceSrv;
......
......@@ -19,19 +19,10 @@ const backendSrv = ({
jest.mock('../services', () => ({
getBackendSrv: () => backendSrv,
}));
jest.mock('..', () => ({
config: {
bootData: {
user: {
orgId: 77,
},
},
datasources: {
sample: {
id: 8674,
},
},
getDataSourceSrv: () => {
return {
getInstanceSettings: () => ({ id: 8674 }),
};
},
}));
......@@ -46,6 +37,7 @@ describe('DataSourceWithBackend', () => {
mockDatasourceRequest.mockReset();
mockDatasourceRequest.mockReturnValue(Promise.resolve({}));
const ds = new MyDataSource(settings);
ds.query({
maxDataPoints: 10,
intervalMs: 5000,
......@@ -64,7 +56,6 @@ describe('DataSourceWithBackend', () => {
"datasourceId": 1234,
"intervalMs": 5000,
"maxDataPoints": 10,
"orgId": 77,
"refId": "A",
},
Object {
......@@ -72,7 +63,6 @@ describe('DataSourceWithBackend', () => {
"datasourceId": 8674,
"intervalMs": 5000,
"maxDataPoints": 10,
"orgId": 77,
"refId": "B",
},
],
......
......@@ -9,8 +9,7 @@ import {
} from '@grafana/data';
import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { config } from '..';
import { getBackendSrv } from '../services';
import { getBackendSrv, getDataSourceSrv } from '../services';
import { toDataQueryResponse } from './queryResponse';
const ExpressionDatasourceID = '__expr__';
......@@ -57,34 +56,37 @@ export class DataSourceWithBackend<
*/
query(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse> {
const { intervalMs, maxDataPoints, range, requestId } = request;
const orgId = config.bootData.user.orgId;
let targets = request.targets;
if (this.filterQuery) {
targets = targets.filter(q => this.filterQuery!(q));
}
const queries = targets.map(q => {
let datasourceId = this.id;
if (q.datasource === ExpressionDatasourceID) {
return {
...q,
datasourceId,
orgId,
};
}
if (q.datasource) {
const dsName = q.datasource === 'default' ? config.defaultDatasource : q.datasource;
const ds = config.datasources[dsName];
const ds = getDataSourceSrv().getInstanceSettings(q.datasource);
if (!ds) {
throw new Error('Unknown Datasource: ' + q.datasource);
}
datasourceId = ds.id;
}
return {
...this.applyTemplateVariables(q, request.scopedVars),
datasourceId,
intervalMs,
maxDataPoints,
orgId,
};
});
......@@ -93,9 +95,8 @@ export class DataSourceWithBackend<
return of({ data: [] });
}
const body: any = {
queries,
};
const body: any = { queries };
if (range) {
body.range = range;
body.from = range.from.valueOf().toString();
......
......@@ -26,6 +26,7 @@ describe('getAlertingValidationMessage', () => {
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getInstanceSettings: (() => {}) as any,
getAll(): DataSourceInstanceSettings[] {
return [];
},
......@@ -66,6 +67,7 @@ describe('getAlertingValidationMessage', () => {
return Promise.resolve(alertingDatasource);
},
getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
......@@ -96,6 +98,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = {
get: getMock,
getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
......@@ -128,6 +131,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = {
get: getMock,
getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
......@@ -160,6 +164,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = {
get: getMock,
getDataSourceSettingsByUid(): any {},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
......
......@@ -214,7 +214,7 @@ function updateFrontendSettings() {
.then((settings: any) => {
config.datasources = settings.datasources;
config.defaultDatasource = settings.defaultDatasource;
getDatasourceSrv().init();
getDatasourceSrv().init(config.datasources, settings.defaultDatasource);
});
}
......
......@@ -82,7 +82,9 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope {
if (this.names) {
return Promise.resolve(this.names);
}
const names: Array<SelectableValue<string>> = [];
for (const [key, ds] of Object.entries(config.datasources)) {
if (ds.meta.live) {
try {
......@@ -99,6 +101,7 @@ export class GrafanaLiveDataSourceScope extends GrafanaLiveScope {
}
}
}
return (this.names = names);
}
}
......
......@@ -2,7 +2,6 @@
import sortBy from 'lodash/sortBy';
import coreModule from 'app/core/core_module';
// Services & Utils
import config from 'app/core/config';
import { importDataSourcePlugin } from './plugin_loader';
import {
DataSourceSrv as DataSourceService,
......@@ -18,47 +17,74 @@ import { expressionDatasource } from 'app/features/expressions/ExpressionDatasou
import { DataSourceVariableModel } from '../variables/types';
export class DatasourceSrv implements DataSourceService {
datasources: Record<string, DataSourceApi> = {};
private datasources: Record<string, DataSourceApi> = {};
private settingsMapByName: Record<string, DataSourceInstanceSettings> = {};
private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {};
private defaultName = '';
/** @ngInject */
constructor(
private $injector: auto.IInjectorService,
private $rootScope: GrafanaRootScope,
private templateSrv: TemplateSrv
) {
this.init();
}
) {}
init() {
init(settingsMapByName: Record<string, DataSourceInstanceSettings>, defaultName: string) {
this.datasources = {};
this.settingsMapByUid = {};
this.settingsMapByName = settingsMapByName;
this.defaultName = defaultName;
for (const dsSettings of Object.values(settingsMapByName)) {
this.settingsMapByUid[dsSettings.uid] = dsSettings;
}
}
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined {
return Object.values(config.datasources).find(ds => ds.uid === uid);
return this.settingsMapByUid[uid];
}
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
if (!name) {
return this.get(config.defaultDatasource);
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined {
if (nameOrUid === 'default' || nameOrUid === null || nameOrUid === undefined) {
return this.settingsMapByName[this.defaultName];
}
return this.settingsMapByUid[nameOrUid] ?? this.settingsMapByName[nameOrUid];
}
get(nameOrUid?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
if (!nameOrUid) {
return this.get(this.defaultName);
}
// Check if nameOrUid matches a uid and then get the name
const byUid = this.settingsMapByUid[nameOrUid];
if (byUid) {
nameOrUid = byUid.name;
}
// This check is duplicated below, this is here mainly as performance optimization to skip interpolation
if (this.datasources[nameOrUid]) {
return Promise.resolve(this.datasources[nameOrUid]);
}
// Interpolation here is to support template variable in data source selection
name = this.templateSrv.replace(name, scopedVars, (value: any[]) => {
nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, (value: any[]) => {
if (Array.isArray(value)) {
return value[0];
}
return value;
});
if (name === 'default') {
return this.get(config.defaultDatasource);
if (nameOrUid === 'default') {
return this.get(this.defaultName);
}
if (this.datasources[name]) {
return Promise.resolve(this.datasources[name]);
if (this.datasources[nameOrUid]) {
return Promise.resolve(this.datasources[nameOrUid]);
}
return this.loadDatasource(name);
return this.loadDatasource(nameOrUid);
}
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
......@@ -68,7 +94,7 @@ export class DatasourceSrv implements DataSourceService {
return Promise.resolve(expressionDatasource);
}
const dsConfig = config.datasources[name];
const dsConfig = this.settingsMapByName[name];
if (!dsConfig) {
return Promise.reject({ message: `Datasource named ${name} was not found` });
}
......@@ -101,8 +127,7 @@ export class DatasourceSrv implements DataSourceService {
}
getAll(): DataSourceInstanceSettings[] {
const { datasources } = config;
return Object.keys(datasources).map(name => datasources[name]);
return Object.values(this.settingsMapByName);
}
getExternal(): DataSourceInstanceSettings[] {
......@@ -115,7 +140,7 @@ export class DatasourceSrv implements DataSourceService {
this.addDataSourceVariables(sources);
Object.values(config.datasources).forEach(value => {
Object.values(this.settingsMapByName).forEach(value => {
if (value.meta?.annotations) {
sources.push(value);
}
......@@ -127,7 +152,7 @@ export class DatasourceSrv implements DataSourceService {
getMetricSources(options?: { skipVariables?: boolean }) {
const metricSources: DataSourceSelectItem[] = [];
Object.entries(config.datasources).forEach(([key, value]) => {
Object.entries(this.settingsMapByName).forEach(([key, value]) => {
if (value.meta?.metrics) {
let metricSource: DataSourceSelectItem = { value: key, name: key, meta: value.meta, sort: key };
......@@ -142,7 +167,7 @@ export class DatasourceSrv implements DataSourceService {
metricSources.push(metricSource);
if (key === config.defaultDatasource) {
if (key === this.defaultName) {
metricSource = { value: null, name: 'default', meta: value.meta, sort: key };
metricSources.push(metricSource);
}
......@@ -172,9 +197,9 @@ export class DatasourceSrv implements DataSourceService {
.getVariables()
.filter(variable => variable.type === 'datasource')
.forEach((variable: DataSourceVariableModel) => {
const first = variable.current.value === 'default' ? config.defaultDatasource : variable.current.value;
const first = variable.current.value === 'default' ? this.defaultName : variable.current.value;
const index = (first as unknown) as string;
const ds = config.datasources[index];
const ds = this.settingsMapByName[index];
if (ds) {
const key = `$${variable.name}`;
......
import config from 'app/core/config';
import 'app/features/plugins/datasource_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DataSourcePluginMeta, PluginMeta } from '@grafana/data';
import { DataSourceInstanceSettings, DataSourcePlugin, DataSourcePluginMeta, PluginMeta } from '@grafana/data';
// Datasource variable $datasource with current value 'BBB'
const templateSrv: any = {
......@@ -14,41 +13,72 @@ const templateSrv: any = {
},
},
],
replace: (v: string) => v,
};
class TestDataSource {
constructor(public instanceSettings: DataSourceInstanceSettings) {}
}
jest.mock('../plugin_loader', () => ({
importDataSourcePlugin: () => {
return Promise.resolve(new DataSourcePlugin(TestDataSource as any));
},
}));
describe('datasource_srv', () => {
const _datasourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv);
const datasources = {
buildIn: {
id: 1,
uid: '1',
type: 'b',
name: 'buildIn',
meta: { builtIn: true } as DataSourcePluginMeta,
jsonData: {},
},
external1: {
id: 2,
uid: '2',
type: 'e',
name: 'external1',
meta: { builtIn: false } as DataSourcePluginMeta,
jsonData: {},
},
external2: {
id: 3,
uid: '3',
type: 'e2',
name: 'external2',
meta: {} as PluginMeta,
jsonData: {},
},
};
describe('when loading external datasources', () => {
beforeEach(() => {
config.datasources = {
buildInDs: {
id: 1,
uid: '1',
type: 'b',
name: 'buildIn',
meta: { builtIn: true } as DataSourcePluginMeta,
jsonData: {},
},
nonBuildIn: {
id: 2,
uid: '2',
type: 'e',
name: 'external1',
meta: { builtIn: false } as DataSourcePluginMeta,
jsonData: {},
},
nonExplore: {
id: 3,
uid: '3',
type: 'e2',
name: 'external2',
meta: {} as PluginMeta,
jsonData: {},
},
};
beforeEach(() => {
_datasourceSrv.init(datasources, 'external1');
});
describe('when getting data source class instance', () => {
it('should load plugin and create instance and set meta', async () => {
const ds = (await _datasourceSrv.get('external1')) as any;
expect(ds.meta).toBe(datasources.external1.meta);
expect(ds.instanceSettings).toBe(datasources.external1);
// validate that it caches instance
const ds2 = await _datasourceSrv.get('external1');
expect(ds).toBe(ds2);
});
it('should be able to load data source using uid as well', async () => {
const dsByUid = await _datasourceSrv.get('2');
const dsByName = await _datasourceSrv.get('external1');
expect(dsByUid.meta).toBe(datasources.external1.meta);
expect(dsByUid).toBe(dsByName);
});
});
describe('when getting external metric sources', () => {
it('should return list of explore sources', () => {
const externalSources = _datasourceSrv.getExternal();
expect(externalSources.length).toBe(2);
......@@ -59,45 +89,48 @@ describe('datasource_srv', () => {
describe('when loading metric sources', () => {
let metricSources: any;
const unsortedDatasources = {
mmm: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
'--Grafana--': {
type: 'grafana',
meta: { builtIn: true, metrics: { m: 1 }, id: 'grafana' },
},
'--Mixed--': {
type: 'test-db',
meta: { builtIn: true, metrics: { m: 1 }, id: 'mixed' },
},
ZZZ: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
aaa: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
BBB: {
type: 'test-db',
meta: { metrics: { m: 1 } },
},
};
beforeEach(() => {
config.datasources = unsortedDatasources as any;
_datasourceSrv.init(
{
mmm: {
type: 'test-db',
meta: { metrics: true } as any,
},
'--Grafana--': {
type: 'grafana',
meta: { builtIn: true, metrics: true, id: 'grafana' },
},
'--Mixed--': {
type: 'test-db',
meta: { builtIn: true, metrics: true, id: 'mixed' },
},
ZZZ: {
type: 'test-db',
meta: { metrics: true },
},
aaa: {
type: 'test-db',
meta: { metrics: true },
},
BBB: {
type: 'test-db',
meta: { metrics: true },
},
} as any,
'BBB'
);
metricSources = _datasourceSrv.getMetricSources({});
config.defaultDatasource = 'BBB';
});
it('should return a list of sources sorted case insensitively with builtin sources last', () => {
expect(metricSources[1].name).toBe('aaa');
expect(metricSources[2].name).toBe('BBB');
expect(metricSources[3].name).toBe('mmm');
expect(metricSources[4].name).toBe('ZZZ');
expect(metricSources[5].name).toBe('--Grafana--');
expect(metricSources[6].name).toBe('--Mixed--');
expect(metricSources[3].name).toBe('default');
expect(metricSources[4].name).toBe('mmm');
expect(metricSources[5].name).toBe('ZZZ');
expect(metricSources[6].name).toBe('--Grafana--');
expect(metricSources[7].name).toBe('--Mixed--');
});
it('should set default data source', () => {
......
......@@ -62,6 +62,8 @@ export class GrafanaCtrl {
setDashboardSrv(dashboardSrv);
setLegacyAngularInjector($injector);
datasourceSrv.init(config.datasources, config.defaultDatasource);
locationUtil.initialize({
getConfig: () => config,
getTimeRangeForUrl: getTimeSrv().timeRangeForUrl,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment