Plugins: Add CDN support to sandboxed frontend plugins (#70608)

This commit is contained in:
Esteban Beltran 2023-06-28 15:58:37 +02:00 committed by GitHub
parent 48db23b32f
commit 5732fc7b2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 101 additions and 62 deletions

View File

@ -1,29 +1,19 @@
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { translateForCDN, extractPluginIdVersionFromUrl } from './pluginCDN'; import { extractPluginIdVersionFromUrl, transformPluginSourceForCDN } from './utils';
describe('Plugin CDN', () => {
describe('translateForCDN', () => { describe('Plugin CDN Utils', () => {
const load = { describe('transformPluginSourceForCdn', () => {
name: 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js', // const localUrl =
address: 'http://my-host.com/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js', // 'http://localhost:3000/public/plugin-cdn/grafana-worldmap-panel/0.3.3/grafana-worldmap-panel/module.js';
source: 'public/plugins/grafana-worldmap-panel/template.html', const pluginId = 'grafana-worldmap-panel';
metadata: { const version = '0.3.3';
extension: '',
deps: [],
format: 'amd',
loader: 'cdn-loader',
encapsulateGlobal: false,
cjsRequireDetection: true,
cjsDeferDepsExecute: false,
esModule: true,
authorization: false,
},
};
config.pluginsCDNBaseURL = 'http://my-host.com'; config.pluginsCDNBaseURL = 'http://my-host.com';
it('should update the default local path to use the CDN path', () => { it('should update the default local path to use the CDN path', () => {
const translatedLoad = translateForCDN({ const translatedLoad = transformPluginSourceForCDN({
...load, pluginId,
version,
source: 'public/plugins/grafana-worldmap-panel/template.html', source: 'public/plugins/grafana-worldmap-panel/template.html',
}); });
expect(translatedLoad).toBe( expect(translatedLoad).toBe(
@ -40,7 +30,7 @@ describe('Plugin CDN', () => {
const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html";
const img = "<img src='http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/data/myimage.jpg'>"; const img = "<img src='http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/data/myimage.jpg'>";
`; `;
const translatedLoad = translateForCDN({ ...load, source }); const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source });
expect(translatedLoad).toBe(expectedSource); expect(translatedLoad).toBe(expectedSource);
}); });
@ -53,7 +43,7 @@ describe('Plugin CDN', () => {
const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html"; const a = "http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/template.html";
const img = "<img src='http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/data/myimage.jpg'>"; const img = "<img src='http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/data/myimage.jpg'>";
`; `;
const translatedLoad = translateForCDN({ ...load, source }); const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source });
expect(translatedLoad).toBe(expectedSource); expect(translatedLoad).toBe(expectedSource);
}); });
@ -72,7 +62,8 @@ describe('Plugin CDN', () => {
".json" ".json"
) )
`; `;
const translatedLoad = translateForCDN({ ...load, source }); const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source });
expect(translatedLoad).toBe(expectedSource); expect(translatedLoad).toBe(expectedSource);
}); });
@ -85,18 +76,19 @@ describe('Plugin CDN', () => {
Zn(t,e)},t.Rectangle=ui,t.rectangle=function(t,e){return new ui(t,e)},t.Map=He,t.map=function(t,e){return new He(t,e)}}(e)}])}); Zn(t,e)},t.Rectangle=ui,t.rectangle=function(t,e){return new ui(t,e)},t.Map=He,t.map=function(t,e){return new He(t,e)}}(e)}])});
//# sourceMappingURL=http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js.map //# sourceMappingURL=http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js.map
`; `;
const translatedLoad = translateForCDN({ ...load, source }); const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source });
expect(translatedLoad).toBe(expectedSource); expect(translatedLoad).toBe(expectedSource);
}); });
it('should replace css paths', () => { it('should replace css paths', () => {
const source = `(0,o.loadPluginCss)({dark:"plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`; const source = `(0,o.loadPluginCss)({dark:"plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`;
const expectedSource = `(0,o.loadPluginCss)({dark:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`; const expectedSource = `(0,o.loadPluginCss)({dark:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.dark.css",light:"http://my-host.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/css/worldmap.light.css"}),`;
const translatedLoad = translateForCDN({ ...load, source }); const translatedLoad = transformPluginSourceForCDN({ pluginId, version, source });
expect(translatedLoad).toBe(expectedSource); expect(translatedLoad).toBe(expectedSource);
}); });
}); });
describe('extractPluginIdVersionFromUrl', () => { describe('extractPluginIdVersionFromUrl', () => {
it('should extract the plugin id and version from a path', () => { it('should extract the plugin id and version from a path', () => {
const source = const source =

View File

@ -0,0 +1,45 @@
import { config } from '@grafana/runtime';
import { PLUGIN_CDN_URL_KEY } from '../constants';
/*
Given an "expected" address of `http://localhost/public/plugin-cdn/{pluginId}/{version}/public/plugins/{pluginId}`
this function will return the plugin id and version.
*/
export function extractPluginIdVersionFromUrl(address: string) {
const path = new URL(address).pathname;
const match = path.split('/');
return { id: match[3], version: match[4] };
}
/*
Transforms plugin's source for CDNs loa.
Plugins that require loading via a CDN need to have their asset paths translated to point to the configured CDN.
e.g. public/plugins/my-plugin/data/ -> http://my-host.com/my-plugin/0.3.3/public/plugins/my-plugin/data/
*/
export function transformPluginSourceForCDN({
pluginId,
version,
source,
}: {
pluginId: string;
version: string;
source: string;
}): string {
const baseAddress = `${config.pluginsCDNBaseURL}/${pluginId}/${version}`;
// handle basic asset paths that include public/plugins
let newSource = source;
newSource = source.replace(/(\/?)(public\/plugins)/g, `${baseAddress}/$2`);
// handle custom plugin css (light and dark themes)
newSource = newSource.replace(/(["|'])(plugins\/.+?.css)(["|'])/g, `$1${baseAddress}/public/$2$3`);
// handle external sourcemap links
newSource = newSource.replace(
/(\/\/#\ssourceMappingURL=)(.+)\.map/g,
`$1${baseAddress}/public/plugins/${pluginId}/$2.map`
);
return newSource;
}
export function getPluginCdnResourceUrl(localPath: string): string {
const pluginPath = localPath.split(`/public/${PLUGIN_CDN_URL_KEY}/`);
return `${config.pluginsCDNBaseURL}/${pluginPath[1]}`;
}

View File

@ -0,0 +1 @@
export const PLUGIN_CDN_URL_KEY = 'plugin-cdn';

View File

@ -33,6 +33,7 @@ import * as ticks from 'app/core/utils/ticks';
import { GenericDataSourcePlugin } from '../datasources/types'; import { GenericDataSourcePlugin } from '../datasources/types';
import builtInPlugins from './built_in_plugins'; import builtInPlugins from './built_in_plugins';
import { PLUGIN_CDN_URL_KEY } from './constants';
import { sandboxPluginDependencies } from './sandbox/plugin_dependencies'; import { sandboxPluginDependencies } from './sandbox/plugin_dependencies';
import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader'; import { importPluginModuleInSandbox } from './sandbox/sandbox_plugin_loader';
import { locateFromCDN, translateForCDN } from './systemjsPlugins/pluginCDN'; import { locateFromCDN, translateForCDN } from './systemjsPlugins/pluginCDN';
@ -78,7 +79,7 @@ grafanaRuntime.SystemJS.config({
'*.css': { '*.css': {
loader: 'css', loader: 'css',
}, },
'plugin-cdn/*': { [`${PLUGIN_CDN_URL_KEY}/*`]: {
esModule: true, esModule: true,
authorization: false, authorization: false,
loader: 'cdn-loader', loader: 'cdn-loader',

View File

@ -3,6 +3,8 @@ import { ProxyTarget } from '@locker/near-membrane-shared';
import { PluginMeta } from '@grafana/data'; import { PluginMeta } from '@grafana/data';
import { extractPluginIdVersionFromUrl, getPluginCdnResourceUrl, transformPluginSourceForCDN } from '../cdn/utils';
import { PLUGIN_CDN_URL_KEY } from '../constants';
import { getPluginSettings } from '../pluginSettings'; import { getPluginSettings } from '../pluginSettings';
import { getGeneralSandboxDistortionMap } from './distortion_map'; import { getGeneralSandboxDistortionMap } from './distortion_map';
@ -120,11 +122,16 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
}, },
}); });
// fetch and evalute the plugin code inside the sandbox // fetch plugin's code
let pluginCode = '';
try { try {
let pluginCode = await getPluginCode(meta.module); pluginCode = await getPluginCode(meta);
pluginCode = patchPluginSourceMap(meta, pluginCode); } catch (e) {
throw new Error(`Could not load plugin ${meta.id}: ` + e);
reject(new Error(`Could not load plugin ${meta.id}: ` + e));
}
try {
// runs the code inside the sandbox environment // runs the code inside the sandbox environment
// this evaluate will eventually run the `define` function inside // this evaluate will eventually run the `define` function inside
// of endowments. // of endowments.
@ -135,9 +142,26 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise<unknown>
}); });
} }
async function getPluginCode(modulePath: string) { async function getPluginCode(meta: PluginMeta): Promise<string> {
const response = await fetch('public/' + modulePath + '.js'); if (meta.module.includes(`${PLUGIN_CDN_URL_KEY}/`)) {
return await response.text(); // should load plugin from a CDN
const pluginUrl = getPluginCdnResourceUrl(`/public/${meta.module}`) + '.js';
const response = await fetch(pluginUrl);
let pluginCode = await response.text();
const { version } = extractPluginIdVersionFromUrl(pluginUrl);
pluginCode = transformPluginSourceForCDN({
pluginId: meta.id,
version,
source: pluginCode,
});
return pluginCode;
} else {
//local plugin loading
const response = await fetch('public/' + meta.module + '.js');
let pluginCode = await response.text();
pluginCode = patchPluginSourceMap(meta, pluginCode);
return pluginCode;
}
} }
function getActivityErrorHandler(pluginId: string) { function getActivityErrorHandler(pluginId: string) {

View File

@ -1,17 +1,7 @@
import { config } from '@grafana/runtime'; import { extractPluginIdVersionFromUrl, getPluginCdnResourceUrl, transformPluginSourceForCDN } from '../cdn/utils';
import type { SystemJSLoad } from './types'; import type { SystemJSLoad } from './types';
/*
Given an "expected" address of `http://localhost/public/plugin-cdn/{pluginId}/{version}/public/plugins/{pluginId}`
this function will return the plugin id and version.
*/
export function extractPluginIdVersionFromUrl(address: string) {
const path = new URL(address).pathname;
const match = path.split('/');
return { id: match[3], version: match[4] };
}
/* /*
Locate: Overrides the location of the plugin resource Locate: Overrides the location of the plugin resource
Plugins loaded via CDN fall into this plugin via the `plugin-cdn` keyword. Plugins loaded via CDN fall into this plugin via the `plugin-cdn` keyword.
@ -21,27 +11,13 @@ export function extractPluginIdVersionFromUrl(address: string) {
*/ */
export function locateFromCDN(load: SystemJSLoad) { export function locateFromCDN(load: SystemJSLoad) {
const { address } = load; const { address } = load;
const pluginPath = address.split('/public/plugin-cdn/'); return getPluginCdnResourceUrl(address);
return `${config.pluginsCDNBaseURL}/${pluginPath[1]}`;
} }
/* /*
Translate: Returns the translated source from load.source, can also set load.metadata.sourceMap for full source maps support. Translate: Returns the translated source from load.source;
Plugins that require loading via a CDN need to have their asset paths translated to point to the configured CDN.
e.g. public/plugins/my-plugin/data/ -> http://my-host.com/my-plugin/0.3.3/public/plugins/my-plugin/data/
*/ */
export function translateForCDN(load: SystemJSLoad) { export function translateForCDN(load: SystemJSLoad) {
const { id, version } = extractPluginIdVersionFromUrl(load.name); const { id, version } = extractPluginIdVersionFromUrl(load.name);
const baseAddress = `${config.pluginsCDNBaseURL}/${id}/${version}`; return transformPluginSourceForCDN({ pluginId: id, version, source: load.source });
// handle basic asset paths that include public/plugins
load.source = load.source.replace(/(\/?)(public\/plugins)/g, `${baseAddress}/$2`);
// handle custom plugin css (light and dark themes)
load.source = load.source.replace(/(["|'])(plugins\/.+?.css)(["|'])/g, `$1${baseAddress}/public/$2$3`);
// handle external sourcemap links
load.source = load.source.replace(
/(\/\/#\ssourceMappingURL=)(.+)\.map/g,
`$1${baseAddress}/public/plugins/${id}/$2.map`
);
return load.source;
} }