Compare commits
1 Commits
533f74fe7e
...
slice/43-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de957cb971 |
@@ -41,7 +41,20 @@ async function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
||||
? await source.resolveFolderUid()
|
||||
: (source.config?.grafanaConnector?.folderUid || undefined);
|
||||
|
||||
// Resolve the InfluxDB datasource uid by querying the target Grafana, then
|
||||
// rewrite every panel/target/variable on each dashboard. Templates ship a
|
||||
// hardcoded uid that only matches the Grafana they were authored against;
|
||||
// without this rewrite a fresh Grafana renders every panel as
|
||||
// "Datasource <uid> not found". Failure is non-fatal: rewriteDatasourceUid
|
||||
// is a no-op when uid is empty, so panels keep their template uid.
|
||||
const datasourceUid = typeof source.resolveDatasourceUid === 'function'
|
||||
? await source.resolveDatasourceUid()
|
||||
: '';
|
||||
|
||||
for (const dash of dashboards) {
|
||||
if (datasourceUid && typeof source.rewriteDatasourceUid === 'function') {
|
||||
source.rewriteDatasourceUid(dash.dashboard, datasourceUid);
|
||||
}
|
||||
ctx.send({
|
||||
...msg,
|
||||
topic: 'create',
|
||||
|
||||
@@ -350,6 +350,11 @@ class DashboardApi {
|
||||
return `${protocol}://${host}:${port}/api/folders`;
|
||||
}
|
||||
|
||||
grafanaDatasourcesUrl() {
|
||||
const { protocol, host, port } = this.config.grafanaConnector;
|
||||
return `${protocol}://${host}:${port}/api/datasources`;
|
||||
}
|
||||
|
||||
_grafanaJsonHeaders() {
|
||||
const headers = { Accept: 'application/json', 'Content-Type': 'application/json' };
|
||||
const token = this.config.grafanaConnector.bearerToken;
|
||||
@@ -415,6 +420,90 @@ class DashboardApi {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Resolve the target Grafana InfluxDB datasource uid at push time. Templates
|
||||
// ship with a hardcoded uid baked into every panel; that uid only matches the
|
||||
// Grafana instance the templates were authored against. Any other Grafana
|
||||
// (fresh laptop, VPS, rebuilt instance) renders the panels as
|
||||
// "Datasource <uid> not found". Resolution is done once per process and
|
||||
// cached.
|
||||
//
|
||||
// Degradation contract: any failure (no fetch, network error, non-OK
|
||||
// response, no influxdb datasource present) returns '' and the caller leaves
|
||||
// the template's baked-in uid alone. Worst-case behavior is unchanged from
|
||||
// before this resolver existed.
|
||||
async resolveDatasourceUid({ fetchImpl = globalThis.fetch } = {}) {
|
||||
if (this._resolvedDatasourceUid) return this._resolvedDatasourceUid;
|
||||
if (typeof fetchImpl !== 'function') {
|
||||
this.logger.warn('resolveDatasourceUid: no fetch implementation available; leaving template uid intact');
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const uid = await this._lookupInfluxDatasource(fetchImpl);
|
||||
if (uid) {
|
||||
this._resolvedDatasourceUid = uid;
|
||||
return uid;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`resolveDatasourceUid failed (${err?.message || err}); leaving template uid intact`);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async _lookupInfluxDatasource(fetchImpl) {
|
||||
const url = this.grafanaDatasourcesUrl();
|
||||
const headers = this._grafanaJsonHeaders();
|
||||
const res = await fetchImpl(url, { method: 'GET', headers });
|
||||
if (!res?.ok) {
|
||||
this.logger.warn(`resolveDatasourceUid: GET /api/datasources -> ${res?.status}`);
|
||||
return '';
|
||||
}
|
||||
const list = await res.json();
|
||||
const match = Array.isArray(list) && list.find((d) => String(d?.type || '').toLowerCase() === 'influxdb');
|
||||
if (match?.uid) {
|
||||
this.logger.info({ event: 'datasource-resolved', outcome: 'found', name: match.name, uid: match.uid });
|
||||
return match.uid;
|
||||
}
|
||||
this.logger.warn('resolveDatasourceUid: no influxdb datasource on target Grafana');
|
||||
return '';
|
||||
}
|
||||
|
||||
// Rewrite every influxdb datasource.uid on a dashboard (panels, nested row
|
||||
// panels, panel.targets, templating variables) to `uid`. No-op for any
|
||||
// datasource whose type isn't 'influxdb' (e.g. the '-- Grafana --' annotation
|
||||
// datasource) or whose uid is a template variable reference (e.g.
|
||||
// '${datasource}'). No-op when `uid` is falsy.
|
||||
rewriteDatasourceUid(dashboard, uid) {
|
||||
if (!uid || !dashboard) return;
|
||||
const visit = (panels) => {
|
||||
if (!Array.isArray(panels)) return;
|
||||
for (const panel of panels) {
|
||||
if (panel?.datasource && String(panel.datasource.type || '').toLowerCase() === 'influxdb'
|
||||
&& typeof panel.datasource.uid === 'string' && !panel.datasource.uid.startsWith('$')) {
|
||||
panel.datasource.uid = uid;
|
||||
}
|
||||
if (Array.isArray(panel?.targets)) {
|
||||
for (const t of panel.targets) {
|
||||
if (t?.datasource && String(t.datasource.type || '').toLowerCase() === 'influxdb'
|
||||
&& typeof t.datasource.uid === 'string' && !t.datasource.uid.startsWith('$')) {
|
||||
t.datasource.uid = uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(panel?.panels);
|
||||
}
|
||||
};
|
||||
visit(dashboard.panels);
|
||||
const tplList = dashboard?.templating?.list;
|
||||
if (Array.isArray(tplList)) {
|
||||
for (const v of tplList) {
|
||||
if (v?.datasource && String(v.datasource.type || '').toLowerCase() === 'influxdb'
|
||||
&& typeof v.datasource.uid === 'string' && !v.datasource.uid.startsWith('$')) {
|
||||
v.datasource.uid = uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildDashboard({ nodeConfig, positionVsParent }) {
|
||||
const softwareType =
|
||||
nodeConfig?.functionality?.softwareType ||
|
||||
|
||||
174
test/basic/slice49-datasource-resolve.basic.test.js
Normal file
174
test/basic/slice49-datasource-resolve.basic.test.js
Normal file
@@ -0,0 +1,174 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const DashboardApi = require('../../src/specificClass.js');
|
||||
const { registerChild } = require('../../src/commands/handlers.js');
|
||||
|
||||
function makeFetch(routes) {
|
||||
const calls = [];
|
||||
const fetchImpl = async (url, opts = {}) => {
|
||||
const method = opts.method || 'GET';
|
||||
const { pathname } = new URL(url);
|
||||
calls.push({ method, pathname, body: opts.body });
|
||||
const r = routes[`${method} ${pathname}`];
|
||||
if (!r) return { ok: false, status: 404, json: async () => ({ message: 'not found' }) };
|
||||
if (typeof r === 'function') return r();
|
||||
return { ok: r.ok ?? true, status: r.status ?? 200, json: async () => r.body };
|
||||
};
|
||||
fetchImpl.calls = calls;
|
||||
return fetchImpl;
|
||||
}
|
||||
|
||||
function api(grafanaConnector = {}) {
|
||||
return new DashboardApi({ grafanaConnector });
|
||||
}
|
||||
|
||||
test('resolveDatasourceUid returns the first influxdb datasource uid', async () => {
|
||||
const a = api();
|
||||
const fetchImpl = makeFetch({
|
||||
'GET /api/datasources': {
|
||||
body: [
|
||||
{ type: 'prometheus', uid: 'p1' },
|
||||
{ type: 'influxdb', uid: 'dfmpjg9jjvym8b', name: 'influxdb' },
|
||||
{ type: 'influxdb', uid: 'second-one' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const uid = await a.resolveDatasourceUid({ fetchImpl });
|
||||
assert.equal(uid, 'dfmpjg9jjvym8b');
|
||||
});
|
||||
|
||||
test('resolveDatasourceUid is cached → second call makes no further fetch', async () => {
|
||||
const a = api();
|
||||
const fetchImpl = makeFetch({
|
||||
'GET /api/datasources': { body: [{ type: 'influxdb', uid: 'u1' }] },
|
||||
});
|
||||
await a.resolveDatasourceUid({ fetchImpl });
|
||||
await a.resolveDatasourceUid({ fetchImpl });
|
||||
assert.equal(fetchImpl.calls.length, 1);
|
||||
});
|
||||
|
||||
test('resolveDatasourceUid returns empty string when no influxdb datasource exists', async () => {
|
||||
const a = api();
|
||||
const fetchImpl = makeFetch({
|
||||
'GET /api/datasources': { body: [{ type: 'prometheus', uid: 'p1' }] },
|
||||
});
|
||||
const uid = await a.resolveDatasourceUid({ fetchImpl });
|
||||
assert.equal(uid, '');
|
||||
});
|
||||
|
||||
test('resolveDatasourceUid: fetch throws → returns empty string (template uid preserved)', async () => {
|
||||
const a = api();
|
||||
const fetchImpl = async () => { throw new Error('ECONNREFUSED'); };
|
||||
const uid = await a.resolveDatasourceUid({ fetchImpl });
|
||||
assert.equal(uid, '');
|
||||
});
|
||||
|
||||
test('resolveDatasourceUid: no fetch available → returns empty string', async () => {
|
||||
const a = api();
|
||||
const uid = await a.resolveDatasourceUid({ fetchImpl: null });
|
||||
assert.equal(uid, '');
|
||||
});
|
||||
|
||||
test('rewriteDatasourceUid: rewrites panel.datasource.uid for influxdb only', () => {
|
||||
const a = api();
|
||||
const dashboard = {
|
||||
panels: [
|
||||
{ datasource: { type: 'influxdb', uid: 'OLD' } },
|
||||
{ datasource: { type: 'grafana', uid: '-- Grafana --' } },
|
||||
],
|
||||
};
|
||||
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||
assert.equal(dashboard.panels[0].datasource.uid, 'NEW');
|
||||
assert.equal(dashboard.panels[1].datasource.uid, '-- Grafana --');
|
||||
});
|
||||
|
||||
test('rewriteDatasourceUid: rewrites panel.targets[].datasource.uid', () => {
|
||||
const a = api();
|
||||
const dashboard = {
|
||||
panels: [
|
||||
{
|
||||
datasource: { type: 'influxdb', uid: 'OLD' },
|
||||
targets: [
|
||||
{ datasource: { type: 'influxdb', uid: 'OLD' }, query: 'a' },
|
||||
{ datasource: { type: 'influxdb', uid: 'OLD' }, query: 'b' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||
for (const t of dashboard.panels[0].targets) assert.equal(t.datasource.uid, 'NEW');
|
||||
});
|
||||
|
||||
test('rewriteDatasourceUid: descends into nested row panels', () => {
|
||||
const a = api();
|
||||
const dashboard = {
|
||||
panels: [
|
||||
{
|
||||
type: 'row',
|
||||
panels: [
|
||||
{ datasource: { type: 'influxdb', uid: 'OLD' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||
assert.equal(dashboard.panels[0].panels[0].datasource.uid, 'NEW');
|
||||
});
|
||||
|
||||
test('rewriteDatasourceUid: rewrites templating.list[] influxdb variables', () => {
|
||||
const a = api();
|
||||
const dashboard = {
|
||||
panels: [],
|
||||
templating: {
|
||||
list: [
|
||||
{ type: 'query', datasource: { type: 'influxdb', uid: 'OLD' } },
|
||||
{ type: 'constant', datasource: { type: 'prometheus', uid: 'OLD' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||
assert.equal(dashboard.templating.list[0].datasource.uid, 'NEW');
|
||||
assert.equal(dashboard.templating.list[1].datasource.uid, 'OLD');
|
||||
});
|
||||
|
||||
test('rewriteDatasourceUid: leaves template-variable references alone (${datasource})', () => {
|
||||
const a = api();
|
||||
const dashboard = {
|
||||
panels: [{ datasource: { type: 'influxdb', uid: '${datasource}' } }],
|
||||
};
|
||||
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||
assert.equal(dashboard.panels[0].datasource.uid, '${datasource}');
|
||||
});
|
||||
|
||||
test('rewriteDatasourceUid: no-op when uid is falsy (preserves template)', () => {
|
||||
const a = api();
|
||||
const dashboard = { panels: [{ datasource: { type: 'influxdb', uid: 'KEEP' } }] };
|
||||
a.rewriteDatasourceUid(dashboard, '');
|
||||
assert.equal(dashboard.panels[0].datasource.uid, 'KEEP');
|
||||
});
|
||||
|
||||
test('emit path rewrites every upsert dashboard with the resolved datasource uid', async () => {
|
||||
const a = api({ folderTitle: 'EVOLV' });
|
||||
a.resolveFolderUid = async () => 'fld';
|
||||
a.resolveDatasourceUid = async () => 'resolved-ds-uid';
|
||||
|
||||
const childSource = {
|
||||
config: { general: { id: 'm1', name: 'Level' }, functionality: { softwareType: 'measurement' } },
|
||||
};
|
||||
const sent = [];
|
||||
const ctx = { node: { id: 'dapi' }, send: (m) => sent.push(m) };
|
||||
await registerChild(a, { payload: childSource }, ctx);
|
||||
|
||||
assert.ok(sent.length >= 1);
|
||||
for (const m of sent) {
|
||||
const panels = m.payload?.dashboard?.panels || [];
|
||||
for (const p of panels) {
|
||||
if (p?.datasource?.type === 'influxdb') {
|
||||
assert.equal(p.datasource.uid, 'resolved-ds-uid');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user