Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a01197f27 |
@@ -440,7 +440,6 @@ i18next.config.ts @grafana/grafana-frontend-platform
|
||||
/e2e-playwright/dashboards/TestDashboard.json @grafana/dashboards-squad @grafana/grafana-search-navigate-organise
|
||||
/e2e-playwright/dashboards/TestV2Dashboard.json @grafana/dashboards-squad
|
||||
/e2e-playwright/dashboards/V2DashWithRepeats.json @grafana/dashboards-squad
|
||||
/e2e-playwright/dashboards/V2DashWithRowRepeats.json @grafana/dashboards-squad
|
||||
/e2e-playwright/dashboards/V2DashWithTabRepeats.json @grafana/dashboards-squad
|
||||
/e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts @grafana/datapro
|
||||
/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @grafana/grafana-search-navigate-organise
|
||||
|
||||
@@ -71,6 +71,11 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec) (m
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert panels: %w", err)
|
||||
}
|
||||
// Count total panels including those in collapsed rows
|
||||
totalPanelsConverted := countTotalPanels(panels)
|
||||
if totalPanelsConverted < len(in.Elements) {
|
||||
return nil, fmt.Errorf("some panels were not converted from v2alpha1 to v1beta1")
|
||||
}
|
||||
|
||||
if len(panels) > 0 {
|
||||
dashboard["panels"] = panels
|
||||
@@ -193,6 +198,29 @@ func convertLinksToV1(links []dashv2alpha1.DashboardDashboardLink) []map[string]
|
||||
return result
|
||||
}
|
||||
|
||||
// countTotalPanels counts all panels including those nested in collapsed row panels.
|
||||
func countTotalPanels(panels []interface{}) int {
|
||||
count := 0
|
||||
for _, p := range panels {
|
||||
panel, ok := p.(map[string]interface{})
|
||||
if !ok {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a row panel with nested panels
|
||||
if panelType, ok := panel["type"].(string); ok && panelType == "row" {
|
||||
if nestedPanels, ok := panel["panels"].([]interface{}); ok {
|
||||
count += len(nestedPanels)
|
||||
}
|
||||
// Don't count the row itself as a panel element
|
||||
} else {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// convertPanelsFromElementsAndLayout converts V2 layout structures to V1 panel arrays.
|
||||
// V1 only supports a flat array of panels with row panels for grouping.
|
||||
// This function dispatches to the appropriate converter based on layout type:
|
||||
|
||||
@@ -13,10 +13,9 @@ const (
|
||||
)
|
||||
|
||||
// PluginAssetsCalculator is an interface for calculating plugin asset information.
|
||||
// LocalProvider requires this to calculate loading strategy and module hash.
|
||||
// LocalProvider requires this to calculate loading strategy.
|
||||
type PluginAssetsCalculator interface {
|
||||
LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy
|
||||
ModuleHash(ctx context.Context, p pluginstore.Plugin) string
|
||||
}
|
||||
|
||||
// LocalProvider retrieves plugin metadata for locally installed plugins.
|
||||
@@ -27,7 +26,7 @@ type LocalProvider struct {
|
||||
}
|
||||
|
||||
// NewLocalProvider creates a new LocalProvider for locally installed plugins.
|
||||
// pluginAssets is required for calculating loading strategy and module hash.
|
||||
// pluginAssets is required for calculating loading strategy.
|
||||
func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider {
|
||||
return &LocalProvider{
|
||||
store: pluginStore,
|
||||
@@ -43,7 +42,7 @@ func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) (
|
||||
}
|
||||
|
||||
loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin)
|
||||
moduleHash := p.pluginAssets.ModuleHash(ctx, plugin)
|
||||
moduleHash := plugin.ModuleHash
|
||||
|
||||
spec := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash)
|
||||
return &Result{
|
||||
|
||||
@@ -370,7 +370,6 @@
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](https://grafana.com/developers/plugin-tools/how-to-guides/data-source-plugins/add-authentication-for-data-source-plugins).",
|
||||
"required": ["path"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import testV2DashWithRepeats from '../dashboards/V2DashWithRepeats.json';
|
||||
import testV2DashWithRowRepeats from '../dashboards/V2DashWithRowRepeats.json';
|
||||
|
||||
import {
|
||||
checkRepeatedPanelTitles,
|
||||
@@ -11,14 +10,11 @@ import {
|
||||
saveDashboard,
|
||||
importTestDashboard,
|
||||
goToEmbeddedPanel,
|
||||
goToPanelSnapshot,
|
||||
} from './utils';
|
||||
|
||||
const repeatTitleBase = 'repeat - ';
|
||||
const newTitleBase = 'edited rep - ';
|
||||
const repeatOptions = [1, 2, 3, 4];
|
||||
const getTitleInRepeatRow = (rowIndex: number, panelIndex: number) =>
|
||||
`repeated-row-${rowIndex}-repeated-panel-${panelIndex}`;
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
@@ -169,7 +165,9 @@ test.describe(
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody)
|
||||
@@ -219,7 +217,9 @@ test.describe(
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody)
|
||||
@@ -405,143 +405,5 @@ test.describe(
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer).all()
|
||||
).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('can view repeated panel in a repeated row', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - view repeated panel in a repeated row',
|
||||
JSON.stringify(testV2DashWithRowRepeats)
|
||||
);
|
||||
|
||||
// make sure the repeated panel is present in multiple rows
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2)))
|
||||
).toBeVisible();
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
.hover();
|
||||
|
||||
await page.keyboard.press('v');
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2)))
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
).toBeVisible();
|
||||
|
||||
const repeatedPanelUrl = page.url();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// load view panel directly
|
||||
await page.goto(repeatedPanelUrl);
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2)))
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('can view embedded panel in a repeated row', async ({ dashboardPage, selectors, page }) => {
|
||||
const embedPanelTitle = 'embedded-panel';
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - view embedded repeated panel in a repeated row',
|
||||
JSON.stringify(testV2DashWithRowRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
.hover();
|
||||
await page.keyboard.press('p+e');
|
||||
|
||||
await goToEmbeddedPanel(page);
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2)))
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
// there is a bug in the Snapshot feature that prevents the next two tests from passing
|
||||
// tracking issue: https://github.com/grafana/grafana/issues/114509
|
||||
test.skip('can view repeated panel inside snapshot', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - view repeated panel inside snapshot',
|
||||
JSON.stringify(testV2DashWithRowRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
.hover();
|
||||
await page.keyboard.press('p+s');
|
||||
|
||||
// click "Publish snapshot"
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot)
|
||||
.click();
|
||||
|
||||
// click "Copy link" button in the snapshot drawer
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.copyUrlButton)
|
||||
.click();
|
||||
|
||||
await goToPanelSnapshot(page);
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(2, 2)))
|
||||
).not.toBeVisible();
|
||||
});
|
||||
test.skip('can view single panel in a repeated row inside snapshot', async ({ dashboardPage, selectors, page }) => {
|
||||
await importTestDashboard(
|
||||
page,
|
||||
selectors,
|
||||
'Custom grid repeats - view single panel inside snapshot',
|
||||
JSON.stringify(testV2DashWithRowRepeats)
|
||||
);
|
||||
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')).hover();
|
||||
// open panel snapshot
|
||||
await page.keyboard.press('p+s');
|
||||
|
||||
// click "Publish snapshot"
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot)
|
||||
.click();
|
||||
|
||||
// click "Copy link" button
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.copyUrlButton)
|
||||
.click();
|
||||
|
||||
await goToPanelSnapshot(page);
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1'))
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getTitleInRepeatRow(1, 1)))
|
||||
).toBeHidden();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -218,15 +218,6 @@ export async function goToEmbeddedPanel(page: Page) {
|
||||
await page.goto(soloPanelUrl!);
|
||||
}
|
||||
|
||||
export async function goToPanelSnapshot(page: Page) {
|
||||
// extracting snapshot url from clipboard
|
||||
const snapshotUrl = await page.evaluate(() => navigator.clipboard.readText());
|
||||
|
||||
expect(snapshotUrl).toBeDefined();
|
||||
|
||||
await page.goto(snapshotUrl);
|
||||
}
|
||||
|
||||
export async function moveTab(
|
||||
dashboardPage: DashboardPage,
|
||||
page: Page,
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
{
|
||||
"apiVersion": "dashboard.grafana.app/v2beta1",
|
||||
"kind": "Dashboard",
|
||||
"metadata": {
|
||||
"name": "ad8l8fz",
|
||||
"namespace": "default",
|
||||
"uid": "fLb2na54K8NZHvn8LfWGL1jhZh03Hy0xpV1KzMYgAXEX",
|
||||
"resourceVersion": "1",
|
||||
"generation": 2,
|
||||
"creationTimestamp": "2025-11-25T15:52:42Z",
|
||||
"labels": {
|
||||
"grafana.app/deprecatedInternalID": "20"
|
||||
},
|
||||
"annotations": {
|
||||
"grafana.app/createdBy": "user:aerwo725ot62od",
|
||||
"grafana.app/updatedBy": "user:aerwo725ot62od",
|
||||
"grafana.app/updatedTimestamp": "2025-11-25T15:52:42Z",
|
||||
"grafana.app/folder": ""
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"annotations": [
|
||||
{
|
||||
"kind": "AnnotationQuery",
|
||||
"spec": {
|
||||
"builtIn": true,
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"query": {
|
||||
"datasource": {
|
||||
"name": "-- Grafana --"
|
||||
},
|
||||
"group": "grafana",
|
||||
"kind": "DataQuery",
|
||||
"spec": {},
|
||||
"version": "v0"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"cursorSync": "Off",
|
||||
"description": "",
|
||||
"editable": true,
|
||||
"elements": {
|
||||
"panel-1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"hidden": false,
|
||||
"query": {
|
||||
"group": "",
|
||||
"kind": "DataQuery",
|
||||
"spec": {},
|
||||
"version": "v0"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
}
|
||||
],
|
||||
"queryOptions": {},
|
||||
"transformations": []
|
||||
}
|
||||
},
|
||||
"description": "",
|
||||
"id": 4,
|
||||
"links": [],
|
||||
"title": "repeated-row-$c4-repeated-panel-$c3",
|
||||
"vizConfig": {
|
||||
"group": "timeseries",
|
||||
"kind": "VizConfig",
|
||||
"spec": {
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "12.4.0-pre"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"hidden": false,
|
||||
"query": {
|
||||
"group": "",
|
||||
"kind": "DataQuery",
|
||||
"spec": {},
|
||||
"version": "v0"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
}
|
||||
],
|
||||
"queryOptions": {},
|
||||
"transformations": []
|
||||
}
|
||||
},
|
||||
"description": "",
|
||||
"id": 2,
|
||||
"links": [],
|
||||
"title": "single panel row $c4",
|
||||
"vizConfig": {
|
||||
"group": "timeseries",
|
||||
"kind": "VizConfig",
|
||||
"spec": {
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "12.4.0-pre"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"kind": "RowsLayout",
|
||||
"spec": {
|
||||
"rows": [
|
||||
{
|
||||
"kind": "RowsLayoutRow",
|
||||
"spec": {
|
||||
"collapse": false,
|
||||
"layout": {
|
||||
"kind": "GridLayout",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-1"
|
||||
},
|
||||
"height": 10,
|
||||
"repeat": {
|
||||
"direction": "h",
|
||||
"mode": "variable",
|
||||
"value": "c3"
|
||||
},
|
||||
"width": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-2"
|
||||
},
|
||||
"height": 8,
|
||||
"width": 12,
|
||||
"x": 0,
|
||||
"y": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"repeat": {
|
||||
"mode": "variable",
|
||||
"value": "c4"
|
||||
},
|
||||
"title": "Repeated row $c4"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"preload": false,
|
||||
"tags": [],
|
||||
"timeSettings": {
|
||||
"autoRefresh": "",
|
||||
"autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"fiscalYearStartMonth": 0,
|
||||
"from": "now-6h",
|
||||
"hideTimepicker": false,
|
||||
"timezone": "browser",
|
||||
"to": "now"
|
||||
},
|
||||
"title": "test-e2e-repeats",
|
||||
"variables": [
|
||||
{
|
||||
"kind": "CustomVariable",
|
||||
"spec": {
|
||||
"allowCustomValue": true,
|
||||
"current": {
|
||||
"text": ["1", "2", "3", "4"],
|
||||
"value": ["1", "2", "3", "4"]
|
||||
},
|
||||
"hide": "dontHide",
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"name": "c1",
|
||||
"options": [
|
||||
{
|
||||
"selected": true,
|
||||
"text": "1",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "2",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "3",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "4",
|
||||
"value": "4"
|
||||
}
|
||||
],
|
||||
"query": "1,2,3,4",
|
||||
"skipUrlSync": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "CustomVariable",
|
||||
"spec": {
|
||||
"allowCustomValue": true,
|
||||
"current": {
|
||||
"text": ["A", "B", "C", "D"],
|
||||
"value": ["A", "B", "C", "D"]
|
||||
},
|
||||
"hide": "dontHide",
|
||||
"includeAll": true,
|
||||
"multi": true,
|
||||
"name": "c2",
|
||||
"options": [
|
||||
{
|
||||
"selected": true,
|
||||
"text": "A",
|
||||
"value": "A"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "B",
|
||||
"value": "B"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "C",
|
||||
"value": "C"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "D",
|
||||
"value": "D"
|
||||
}
|
||||
],
|
||||
"query": "A,B,C,D",
|
||||
"skipUrlSync": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "CustomVariable",
|
||||
"spec": {
|
||||
"allowCustomValue": true,
|
||||
"current": {
|
||||
"text": ["1", "2", "3", "4"],
|
||||
"value": ["1", "2", "3", "4"]
|
||||
},
|
||||
"hide": "dontHide",
|
||||
"includeAll": false,
|
||||
"multi": true,
|
||||
"name": "c3",
|
||||
"options": [
|
||||
{
|
||||
"selected": true,
|
||||
"text": "1",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "2",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "3",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "4",
|
||||
"value": "4"
|
||||
}
|
||||
],
|
||||
"query": "1, 2, 3, 4",
|
||||
"skipUrlSync": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "CustomVariable",
|
||||
"spec": {
|
||||
"allowCustomValue": true,
|
||||
"current": {
|
||||
"text": ["1", "2", "3", "4"],
|
||||
"value": ["1", "2", "3", "4"]
|
||||
},
|
||||
"hide": "dontHide",
|
||||
"includeAll": false,
|
||||
"multi": true,
|
||||
"name": "c4",
|
||||
"options": [
|
||||
{
|
||||
"selected": true,
|
||||
"text": "1",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "2",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "3",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "4",
|
||||
"value": "4"
|
||||
}
|
||||
],
|
||||
"query": "1, 2, 3, 4",
|
||||
"skipUrlSync": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
#!/bin/bash -e
|
||||
|
||||
PERMISSIONS_OK=0
|
||||
|
||||
@@ -27,14 +26,14 @@ if [ ! -d "$GF_PATHS_PLUGINS" ]; then
|
||||
fi
|
||||
|
||||
if [ ! -z ${GF_AWS_PROFILES+x} ]; then
|
||||
:> "$GF_PATHS_HOME/.aws/credentials"
|
||||
> "$GF_PATHS_HOME/.aws/credentials"
|
||||
|
||||
for profile in ${GF_AWS_PROFILES}; do
|
||||
access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID"
|
||||
secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY"
|
||||
region_varname="GF_AWS_${profile}_REGION"
|
||||
|
||||
if [ ! -z "${!access_key_varname}" ] && [ ! -z "${!secret_key_varname}" ]; then
|
||||
if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then
|
||||
echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials"
|
||||
echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
|
||||
echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials"
|
||||
|
||||
@@ -161,7 +161,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
AliasIDs: panel.AliasIDs,
|
||||
Info: panel.Info,
|
||||
Module: panel.Module,
|
||||
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), panel),
|
||||
ModuleHash: panel.ModuleHash,
|
||||
BaseURL: panel.BaseURL,
|
||||
SkipDataQuery: panel.SkipDataQuery,
|
||||
Suggestions: panel.Suggestions,
|
||||
@@ -527,7 +527,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
||||
JSONData: plugin.JSONData,
|
||||
Signature: plugin.Signature,
|
||||
Module: plugin.Module,
|
||||
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin),
|
||||
ModuleHash: plugin.ModuleHash,
|
||||
BaseURL: plugin.BaseURL,
|
||||
Angular: plugin.Angular,
|
||||
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
||||
@@ -641,7 +641,7 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin,
|
||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
||||
Extensions: plugin.Extensions,
|
||||
Dependencies: plugin.Dependencies,
|
||||
ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin),
|
||||
ModuleHash: plugin.ModuleHash,
|
||||
Translations: plugin.Translations,
|
||||
BuildMode: plugin.BuildMode,
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
@@ -79,8 +77,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
|
||||
|
||||
var pluginsAssets = passets
|
||||
if pluginsAssets == nil {
|
||||
sig := signature.ProvideService(pluginsCfg, statickey.New())
|
||||
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, sig, pluginStore)
|
||||
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, pluginStore)
|
||||
}
|
||||
|
||||
hs := &HTTPServer{
|
||||
@@ -714,6 +711,6 @@ func newPluginAssets() func() *pluginassets.Service {
|
||||
|
||||
func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service {
|
||||
return func() *pluginassets.Service {
|
||||
return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), signature.ProvideService(pCfg, statickey.New()), &pluginstore.FakePluginStore{})
|
||||
return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), &pluginstore.FakePluginStore{})
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
|
||||
Includes: plugin.Includes,
|
||||
BaseUrl: plugin.BaseURL,
|
||||
Module: plugin.Module,
|
||||
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin),
|
||||
ModuleHash: plugin.ModuleHash,
|
||||
DefaultNavUrl: path.Join(hs.Cfg.AppSubURL, plugin.DefaultNavURL),
|
||||
State: plugin.State,
|
||||
Signature: plugin.Signature,
|
||||
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -848,8 +846,7 @@ func Test_PluginsSettings(t *testing.T) {
|
||||
}
|
||||
pCfg := &config.PluginManagementCfg{}
|
||||
pluginCDN := pluginscdn.ProvideService(pCfg)
|
||||
sig := signature.ProvideService(pCfg, statickey.New())
|
||||
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, sig, hs.pluginStore)
|
||||
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, hs.pluginStore)
|
||||
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
|
||||
hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService(
|
||||
hs.Cfg,
|
||||
|
||||
@@ -140,7 +140,9 @@ type Licensing interface {
|
||||
}
|
||||
|
||||
type SignatureCalculator interface {
|
||||
Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error)
|
||||
// Calculate calculates the signature and returns both the signature and the manifest.
|
||||
// The manifest may be nil if the plugin is unsigned or if an error occurred.
|
||||
Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, *PluginManifest, error)
|
||||
}
|
||||
|
||||
type KeyStore interface {
|
||||
|
||||
@@ -216,10 +216,27 @@ func TestLoader_Load(t *testing.T) {
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-app/module.js",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")),
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-app/module.js",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(parentDir, "testdata/includes-symlinks")),
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-app",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1622547655175,
|
||||
Files: map[string]string{
|
||||
"dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303",
|
||||
"dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee",
|
||||
"plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf",
|
||||
"symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048",
|
||||
"text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
},
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/plugins/tracing"
|
||||
"github.com/grafana/grafana/pkg/semconv"
|
||||
)
|
||||
@@ -54,7 +55,7 @@ func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap {
|
||||
}
|
||||
|
||||
if opts.DecorateFuncs == nil {
|
||||
opts.DecorateFuncs = DefaultDecorateFuncs(cfg)
|
||||
opts.DecorateFuncs = DefaultDecorateFuncs(cfg, pluginscdn.ProvideService(cfg))
|
||||
}
|
||||
|
||||
return &Bootstrap{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
)
|
||||
|
||||
// DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage.
|
||||
@@ -28,12 +29,13 @@ func DefaultConstructFunc(cfg *config.PluginManagementCfg, signatureCalculator p
|
||||
}
|
||||
|
||||
// DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage.
|
||||
func DefaultDecorateFuncs(cfg *config.PluginManagementCfg) []DecorateFunc {
|
||||
func DefaultDecorateFuncs(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) []DecorateFunc {
|
||||
return []DecorateFunc{
|
||||
AppDefaultNavURLDecorateFunc,
|
||||
TemplateDecorateFunc,
|
||||
AppChildDecorateFunc(),
|
||||
SkipHostEnvVarsDecorateFunc(cfg),
|
||||
ModuleHashDecorateFunc(cfg, cdn),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,19 +50,30 @@ func NewDefaultConstructor(cfg *config.PluginManagementCfg, signatureCalculator
|
||||
|
||||
// Construct will calculate the plugin's signature state and create the plugin using the pluginFactoryFunc.
|
||||
func (c *DefaultConstructor) Construct(ctx context.Context, src plugins.PluginSource, bundle *plugins.FoundBundle) ([]*plugins.Plugin, error) {
|
||||
sig, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary)
|
||||
// Calculate signature and cache manifest
|
||||
sig, manifest, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary)
|
||||
if err != nil {
|
||||
c.log.Warn("Could not calculate plugin signature state", "pluginId", bundle.Primary.JSONData.ID, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plugin, err := c.pluginFactoryFunc(bundle, src.PluginClass(ctx), sig)
|
||||
if err != nil {
|
||||
c.log.Error("Could not create primary plugin base", "pluginId", bundle.Primary.JSONData.ID, "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plugin.Manifest = manifest
|
||||
|
||||
res := make([]*plugins.Plugin, 0, len(plugin.Children)+1)
|
||||
res = append(res, plugin)
|
||||
res = append(res, plugin.Children...)
|
||||
for _, child := range plugin.Children {
|
||||
// Child plugins use the parent's manifest
|
||||
if child.Parent != nil && child.Parent.Manifest != nil {
|
||||
child.Manifest = child.Parent.Manifest
|
||||
}
|
||||
res = append(res, child)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -145,3 +158,11 @@ func SkipHostEnvVarsDecorateFunc(cfg *config.PluginManagementCfg) DecorateFunc {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ModuleHashDecorateFunc returns a DecorateFunc that calculates and sets the module hash for the plugin.
|
||||
func ModuleHashDecorateFunc(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) DecorateFunc {
|
||||
return func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
|
||||
p.ModuleHash = pluginassets.CalculateModuleHash(p, cfg, cdn)
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
@@ -37,26 +36,6 @@ var (
|
||||
fromSlash = filepath.FromSlash
|
||||
)
|
||||
|
||||
// PluginManifest holds details for the file manifest
|
||||
type PluginManifest struct {
|
||||
Plugin string `json:"plugin"`
|
||||
Version string `json:"version"`
|
||||
KeyID string `json:"keyId"`
|
||||
Time int64 `json:"time"`
|
||||
Files map[string]string `json:"files"`
|
||||
|
||||
// V2 supported fields
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType plugins.SignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
}
|
||||
|
||||
func (m *PluginManifest) IsV2() bool {
|
||||
return strings.HasPrefix(m.ManifestVersion, "2.")
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
kr plugins.KeyRetriever
|
||||
cfg *config.PluginManagementCfg
|
||||
@@ -87,14 +66,14 @@ func DefaultCalculator(cfg *config.PluginManagementCfg) *Signature {
|
||||
|
||||
// readPluginManifest attempts to read and verify the plugin manifest
|
||||
// if any error occurs or the manifest is not valid, this will return an error
|
||||
func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*PluginManifest, error) {
|
||||
func (s *Signature) readPluginManifest(ctx context.Context, body []byte) (*plugins.PluginManifest, error) {
|
||||
block, _ := clearsign.Decode(body)
|
||||
if block == nil {
|
||||
return nil, errors.New("unable to decode manifest")
|
||||
}
|
||||
|
||||
// Convert to a well typed object
|
||||
var manifest PluginManifest
|
||||
var manifest plugins.PluginManifest
|
||||
err := json.Unmarshal(block.Plaintext, &manifest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %w", "Error parsing manifest JSON", err)
|
||||
@@ -111,7 +90,7 @@ var ErrSignatureTypeUnsigned = errors.New("plugin is unsigned")
|
||||
|
||||
// ReadPluginManifestFromFS reads the plugin manifest from the provided plugins.FS.
|
||||
// If the manifest is not found, it will return an error wrapping ErrSignatureTypeUnsigned.
|
||||
func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*PluginManifest, error) {
|
||||
func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS) (*plugins.PluginManifest, error) {
|
||||
f, err := pfs.Open("MANIFEST.txt")
|
||||
if err != nil {
|
||||
if errors.Is(err, plugins.ErrFileNotExist) {
|
||||
@@ -140,9 +119,9 @@ func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, *plugins.PluginManifest, error) {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx, plugin.JSONData.ID); exists {
|
||||
return defaultSignature, nil
|
||||
return defaultSignature, nil, nil
|
||||
}
|
||||
|
||||
manifest, err := s.ReadPluginManifestFromFS(ctx, plugin.FS)
|
||||
@@ -151,29 +130,29 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
s.log.Warn("Plugin is unsigned", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusUnsigned,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
case err != nil:
|
||||
s.log.Warn("Plugin signature is invalid", "id", plugin.JSONData.ID, "err", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
if !manifest.IsV2() {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
fsFiles, err := plugin.FS.Files()
|
||||
if err != nil {
|
||||
return plugins.Signature{}, fmt.Errorf("files: %w", err)
|
||||
return plugins.Signature{}, nil, fmt.Errorf("files: %w", err)
|
||||
}
|
||||
if len(fsFiles) == 0 {
|
||||
s.log.Warn("No plugin file information in directory", "pluginId", plugin.JSONData.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// Make sure the versions all match
|
||||
@@ -181,20 +160,20 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
s.log.Debug("Plugin signature invalid because ID or Version mismatch", "pluginId", plugin.JSONData.ID, "manifestPluginId", manifest.Plugin, "pluginVersion", plugin.JSONData.Info.Version, "manifestPluginVersion", manifest.Version)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusModified,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// Validate that plugin is running within defined root URLs
|
||||
if len(manifest.RootURLs) > 0 {
|
||||
if match, err := urlMatch(manifest.RootURLs, s.cfg.GrafanaAppURL, manifest.SignatureType); err != nil {
|
||||
s.log.Warn("Could not verify if root URLs match", "plugin", plugin.JSONData.ID, "rootUrls", manifest.RootURLs)
|
||||
return plugins.Signature{}, err
|
||||
return plugins.Signature{}, nil, err
|
||||
} else if !match {
|
||||
s.log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.JSONData.ID,
|
||||
"appUrl", s.cfg.GrafanaAppURL, "rootUrls", manifest.RootURLs)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusInvalid,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +186,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
s.log.Debug("Plugin signature invalid", "pluginId", plugin.JSONData.ID, "error", err)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusModified,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
manifestFiles[p] = struct{}{}
|
||||
@@ -236,7 +215,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
s.log.Warn("The following files were not included in the signature", "plugin", plugin.JSONData.ID, "files", unsignedFiles)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureStatusModified,
|
||||
}, nil
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
s.log.Debug("Plugin signature valid", "id", plugin.JSONData.ID)
|
||||
@@ -244,7 +223,7 @@ func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plu
|
||||
Status: plugins.SignatureStatusValid,
|
||||
Type: manifest.SignatureType,
|
||||
SigningOrg: manifest.SignedByOrgName,
|
||||
}, nil
|
||||
}, manifest, nil
|
||||
}
|
||||
|
||||
func verifyHash(mlog log.Logger, plugin plugins.FoundPlugin, path, hash string) error {
|
||||
@@ -321,7 +300,7 @@ func (r invalidFieldErr) Error() string {
|
||||
return fmt.Sprintf("valid manifest field %s is required", r.field)
|
||||
}
|
||||
|
||||
func (s *Signature) validateManifest(ctx context.Context, m PluginManifest, block *clearsign.Block) error {
|
||||
func (s *Signature) validateManifest(ctx context.Context, m plugins.PluginManifest, block *clearsign.Block) error {
|
||||
if len(m.Plugin) == 0 {
|
||||
return invalidFieldErr{field: "plugin"}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestCalculate(t *testing.T) {
|
||||
for _, tc := range tcs {
|
||||
basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
|
||||
s := provideTestServiceWithConfig(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL})
|
||||
sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{
|
||||
sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.ClassExternal
|
||||
},
|
||||
@@ -192,7 +192,7 @@ func TestCalculate(t *testing.T) {
|
||||
|
||||
runningWindows = true
|
||||
s := provideDefaultTestService()
|
||||
sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{
|
||||
sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.ClassExternal
|
||||
},
|
||||
@@ -260,7 +260,7 @@ func TestCalculate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
|
||||
require.NoError(t, err)
|
||||
sig, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{
|
||||
sig, _, err := s.Calculate(context.Background(), &pluginfakes.FakePluginSource{
|
||||
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||
return plugins.ClassExternal
|
||||
},
|
||||
@@ -396,7 +396,7 @@ func TestFSPathSeparatorFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func fileList(manifest *PluginManifest) []string {
|
||||
func fileList(manifest *plugins.PluginManifest) []string {
|
||||
keys := make([]string, 0, len(manifest.Files))
|
||||
for k := range manifest.Files {
|
||||
keys = append(keys, k)
|
||||
@@ -682,52 +682,52 @@ func Test_urlMatch_private(t *testing.T) {
|
||||
func Test_validateManifest(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
manifest *PluginManifest
|
||||
manifest *plugins.PluginManifest
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Empty plugin field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Plugin = "" }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Plugin = "" }),
|
||||
expectedErr: "valid manifest field plugin is required",
|
||||
},
|
||||
{
|
||||
name: "Empty keyId field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.KeyID = "" }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.KeyID = "" }),
|
||||
expectedErr: "valid manifest field keyId is required",
|
||||
},
|
||||
{
|
||||
name: "Empty signedByOrg field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrg = "" }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrg = "" }),
|
||||
expectedErr: "valid manifest field signedByOrg is required",
|
||||
},
|
||||
{
|
||||
name: "Empty signedByOrgName field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrgName = "" }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignedByOrgName = "" }),
|
||||
expectedErr: "valid manifest field SignedByOrgName is required",
|
||||
},
|
||||
{
|
||||
name: "Empty signatureType field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "" }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "" }),
|
||||
expectedErr: "valid manifest field signatureType is required",
|
||||
},
|
||||
{
|
||||
name: "Invalid signatureType field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "invalidSignatureType" }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.SignatureType = "invalidSignatureType" }),
|
||||
expectedErr: "valid manifest field signatureType is required",
|
||||
},
|
||||
{
|
||||
name: "Empty files field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Files = map[string]string{} }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Files = map[string]string{} }),
|
||||
expectedErr: "valid manifest field files is required",
|
||||
},
|
||||
{
|
||||
name: "Empty time field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Time = 0 }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Time = 0 }),
|
||||
expectedErr: "valid manifest field time is required",
|
||||
},
|
||||
{
|
||||
name: "Empty version field",
|
||||
manifest: createV2Manifest(t, func(m *PluginManifest) { m.Version = "" }),
|
||||
manifest: createV2Manifest(t, func(m *plugins.PluginManifest) { m.Version = "" }),
|
||||
expectedErr: "valid manifest field version is required",
|
||||
},
|
||||
}
|
||||
@@ -740,10 +740,10 @@ func Test_validateManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifest {
|
||||
func createV2Manifest(t *testing.T, cbs ...func(*plugins.PluginManifest)) *plugins.PluginManifest {
|
||||
t.Helper()
|
||||
|
||||
m := &PluginManifest{
|
||||
m := &plugins.PluginManifest{
|
||||
Plugin: "grafana-test-app",
|
||||
Version: "2.5.3",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package pluginassets
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
)
|
||||
|
||||
// CalculateModuleHash calculates the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
|
||||
// The module hash is read from the plugin's cached manifest.
|
||||
// For nested plugins, the module hash is read from the root parent plugin's manifest.
|
||||
// If the plugin is unsigned or not a CDN plugin, an empty string is returned.
|
||||
func CalculateModuleHash(p *plugins.Plugin, cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) string {
|
||||
if cfg == nil || !cfg.Features.SriChecksEnabled {
|
||||
return ""
|
||||
}
|
||||
|
||||
if !p.Signature.IsValid() {
|
||||
return ""
|
||||
}
|
||||
|
||||
rootParent := findRootParent(p)
|
||||
if rootParent.Manifest == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if !rootParent.Manifest.IsV2() {
|
||||
return ""
|
||||
}
|
||||
|
||||
if !cdnEnabled(rootParent, cdn) {
|
||||
return ""
|
||||
}
|
||||
|
||||
modulePath := getModulePathInManifest(p, rootParent)
|
||||
moduleHash, ok := rootParent.Manifest.Files[modulePath]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return convertHashForSRI(moduleHash)
|
||||
}
|
||||
|
||||
// findRootParent returns the root parent plugin (the one that contains the manifest).
|
||||
// For non-nested plugins, it returns the plugin itself.
|
||||
func findRootParent(p *plugins.Plugin) *plugins.Plugin {
|
||||
root := p
|
||||
for root.Parent != nil {
|
||||
root = root.Parent
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// getModulePathInManifest returns the path to module.js as it appears in the manifest.
|
||||
// For nested plugins, this is the relative path from the root parent to the plugin's module.js.
|
||||
// For non-nested plugins, this is simply "module.js".
|
||||
func getModulePathInManifest(p *plugins.Plugin, rootParent *plugins.Plugin) string {
|
||||
if p == rootParent {
|
||||
return "module.js"
|
||||
}
|
||||
|
||||
// Calculate the relative path from root parent to this plugin
|
||||
relPath, err := rootParent.FS.Rel(p.FS.Base())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// MANIFEST.txt uses forward slashes as path separators
|
||||
pluginRootPath := filepath.ToSlash(relPath)
|
||||
return path.Join(pluginRootPath, "module.js")
|
||||
}
|
||||
|
||||
// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks.
|
||||
func convertHashForSRI(h string) string {
|
||||
hb, err := hex.DecodeString(h)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return "sha256-" + base64.StdEncoding.EncodeToString(hb)
|
||||
}
|
||||
|
||||
// cdnEnabled checks if a plugin is loaded via CDN
|
||||
func cdnEnabled(p *plugins.Plugin, cdn *pluginscdn.Service) bool {
|
||||
return p.FS.Type().CDN() || cdn.PluginSupported(p.ID)
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package pluginassets
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
)
|
||||
|
||||
func TestConvertHashForSRI(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
hash string
|
||||
expHash string
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811",
|
||||
expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=",
|
||||
},
|
||||
{
|
||||
hash: "not-a-valid-hash",
|
||||
expErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.hash, func(t *testing.T) {
|
||||
r := convertHashForSRI(tc.hash)
|
||||
if tc.expErr {
|
||||
// convertHashForSRI returns empty string on error
|
||||
require.Empty(t, r)
|
||||
} else {
|
||||
require.Equal(t, tc.expHash, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateModuleHash(t *testing.T) {
|
||||
const (
|
||||
pluginID = "grafana-test-datasource"
|
||||
parentPluginID = "grafana-test-app"
|
||||
)
|
||||
|
||||
// Helper to create a plugin with manifest
|
||||
createPluginWithManifest := func(id string, manifest *plugins.PluginManifest, parent *plugins.Plugin) *plugins.Plugin {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: id,
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: manifest,
|
||||
}
|
||||
if parent != nil {
|
||||
p.Parent = parent
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Helper to create a v2 manifest
|
||||
createV2Manifest := func(files map[string]string) *plugins.PluginManifest {
|
||||
return &plugins.PluginManifest{
|
||||
ManifestVersion: "2.0.0",
|
||||
Files: files,
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
plugin *plugins.Plugin
|
||||
cfg *config.PluginManagementCfg
|
||||
cdn *pluginscdn.Service
|
||||
expModuleHash string
|
||||
}{
|
||||
{
|
||||
name: "should return empty string when cfg is nil",
|
||||
plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{
|
||||
"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
|
||||
}), nil),
|
||||
cfg: nil,
|
||||
cdn: nil,
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "should return empty string when SRI checks are disabled",
|
||||
plugin: createPluginWithManifest(pluginID, createV2Manifest(map[string]string{
|
||||
"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
|
||||
}), nil),
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: false}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "should return empty string for unsigned plugin",
|
||||
plugin: &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusUnsigned,
|
||||
Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}),
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")),
|
||||
},
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "should return module hash for valid plugin",
|
||||
plugin: &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: createV2Manifest(map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"}),
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")),
|
||||
},
|
||||
cfg: &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
Features: config.Features{SriChecksEnabled: true},
|
||||
PluginSettings: config.PluginSettings{
|
||||
pluginID: {"cdn": "true"},
|
||||
},
|
||||
},
|
||||
cdn: func() *pluginscdn.Service {
|
||||
cfg := &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
PluginSettings: config.PluginSettings{
|
||||
pluginID: {"cdn": "true"},
|
||||
},
|
||||
}
|
||||
return pluginscdn.ProvideService(cfg)
|
||||
}(),
|
||||
expModuleHash: "sha256-WJG1tSLV3whtD/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgM=",
|
||||
},
|
||||
{
|
||||
name: "should return empty string when manifest is nil",
|
||||
plugin: &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: nil,
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")),
|
||||
},
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "should return empty string for v1 manifest",
|
||||
plugin: &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: &plugins.PluginManifest{
|
||||
ManifestVersion: "1.0.0",
|
||||
Files: map[string]string{"module.js": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"},
|
||||
},
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")),
|
||||
},
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "should return empty string when module.js is not in manifest",
|
||||
plugin: &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}),
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid")),
|
||||
},
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "missing module.js entry from MANIFEST.txt should not return module hash",
|
||||
plugin: &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: createV2Manifest(map[string]string{"plugin.json": "129fab4e0584d18c778ebdfa5fe1a68edf2e5c5aeb8290b2c68182c857cb59f8"}),
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js")),
|
||||
},
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "signed status but missing MANIFEST.txt should not return module hash",
|
||||
plugin: &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: nil,
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt")),
|
||||
},
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
// parentPluginID (/)
|
||||
// └── pluginID (/datasource)
|
||||
name: "nested plugin should return module hash from parent MANIFEST.txt",
|
||||
plugin: func() *plugins.Plugin {
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: parentPluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: createV2Manifest(map[string]string{
|
||||
"module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a",
|
||||
"datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711",
|
||||
}),
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")),
|
||||
}
|
||||
return &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Parent: parent,
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource")),
|
||||
}
|
||||
}(),
|
||||
cfg: &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
Features: config.Features{SriChecksEnabled: true},
|
||||
PluginSettings: config.PluginSettings{
|
||||
pluginID: {"cdn": "true"},
|
||||
parentPluginID: {"cdn": "true"},
|
||||
},
|
||||
},
|
||||
cdn: func() *pluginscdn.Service {
|
||||
cfg := &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
PluginSettings: config.PluginSettings{
|
||||
pluginID: {"cdn": "true"},
|
||||
parentPluginID: {"cdn": "true"},
|
||||
},
|
||||
}
|
||||
return pluginscdn.ProvideService(cfg)
|
||||
}(),
|
||||
expModuleHash: "sha256-BNcNsJHZbEd1+zK6Wo+EzCKJPrQ6/bZJcmZh1EJcZxE=",
|
||||
},
|
||||
{
|
||||
// parentPluginID (/)
|
||||
// └── pluginID (/panels/one)
|
||||
name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt",
|
||||
plugin: func() *plugins.Plugin {
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: parentPluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: createV2Manifest(map[string]string{
|
||||
"module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a",
|
||||
"panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f",
|
||||
"datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711",
|
||||
}),
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")),
|
||||
}
|
||||
return &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Parent: parent,
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")),
|
||||
}
|
||||
}(),
|
||||
cfg: &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
Features: config.Features{SriChecksEnabled: true},
|
||||
PluginSettings: config.PluginSettings{
|
||||
pluginID: {"cdn": "true"},
|
||||
parentPluginID: {"cdn": "true"},
|
||||
},
|
||||
},
|
||||
cdn: func() *pluginscdn.Service {
|
||||
cfg := &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
PluginSettings: config.PluginSettings{
|
||||
pluginID: {"cdn": "true"},
|
||||
parentPluginID: {"cdn": "true"},
|
||||
},
|
||||
}
|
||||
return pluginscdn.ProvideService(cfg)
|
||||
}(),
|
||||
expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=",
|
||||
},
|
||||
{
|
||||
// grand-parent-app (/)
|
||||
// ├── parent-datasource (/datasource)
|
||||
// │ └── child-panel (/datasource/panels/one)
|
||||
name: "nested plugin of a nested plugin should return module hash from grandparent MANIFEST.txt",
|
||||
plugin: func() *plugins.Plugin {
|
||||
grandparent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: "grand-parent-app"},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: createV2Manifest(map[string]string{
|
||||
"module.js": "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a",
|
||||
"datasource/module.js": "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711",
|
||||
"datasource/panels/one/module.js": "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f",
|
||||
}),
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested")),
|
||||
}
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: "parent-datasource"},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Parent: grandparent,
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource")),
|
||||
}
|
||||
return &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: "child-panel"},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Parent: parent,
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one")),
|
||||
}
|
||||
}(),
|
||||
cfg: &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
Features: config.Features{SriChecksEnabled: true},
|
||||
PluginSettings: config.PluginSettings{
|
||||
"child-panel": {"cdn": "true"},
|
||||
"parent-datasource": {"cdn": "true"},
|
||||
"grand-parent-app": {"cdn": "true"},
|
||||
},
|
||||
},
|
||||
cdn: func() *pluginscdn.Service {
|
||||
cfg := &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.example.com",
|
||||
PluginSettings: config.PluginSettings{
|
||||
"child-panel": {"cdn": "true"},
|
||||
"parent-datasource": {"cdn": "true"},
|
||||
"grand-parent-app": {"cdn": "true"},
|
||||
},
|
||||
}
|
||||
return pluginscdn.ProvideService(cfg)
|
||||
}(),
|
||||
expModuleHash: "sha256-y9GsIoRkWg4emocipyn1vN0rgxIicocJxjYL7s3WFD8=",
|
||||
},
|
||||
{
|
||||
name: "nested plugin should not return module hash when parent manifest is nil",
|
||||
plugin: func() *plugins.Plugin {
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: parentPluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Manifest: nil, // Parent has no manifest
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested")),
|
||||
}
|
||||
return &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: pluginID},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
Parent: parent,
|
||||
FS: plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one")),
|
||||
}
|
||||
}(),
|
||||
cfg: &config.PluginManagementCfg{Features: config.Features{SriChecksEnabled: true}},
|
||||
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{}),
|
||||
expModuleHash: "",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := CalculateModuleHash(tc.plugin, tc.cfg, tc.cdn)
|
||||
require.Equal(t, tc.expModuleHash, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
+25
-2
@@ -40,6 +40,7 @@ type Plugin struct {
|
||||
Pinned bool
|
||||
|
||||
// Signature fields
|
||||
Manifest *PluginManifest
|
||||
Signature SignatureStatus
|
||||
SignatureType SignatureType
|
||||
SignatureOrg string
|
||||
@@ -48,8 +49,9 @@ type Plugin struct {
|
||||
Error *Error
|
||||
|
||||
// SystemJS fields
|
||||
Module string
|
||||
BaseURL string
|
||||
Module string
|
||||
ModuleHash string
|
||||
BaseURL string
|
||||
|
||||
Angular AngularMeta
|
||||
|
||||
@@ -532,3 +534,24 @@ func (pt Type) IsValid() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PluginManifest holds details for the file manifest
|
||||
type PluginManifest struct {
|
||||
Plugin string `json:"plugin"`
|
||||
Version string `json:"version"`
|
||||
KeyID string `json:"keyId"`
|
||||
Time int64 `json:"time"`
|
||||
Files map[string]string `json:"files"`
|
||||
|
||||
// V2 supported fields
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType SignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
}
|
||||
|
||||
// IsV2 returns true if the manifest is version 2.x
|
||||
func (m *PluginManifest) IsV2() bool {
|
||||
return strings.HasPrefix(m.ManifestVersion, "2.")
|
||||
}
|
||||
|
||||
Generated
+6
-6
@@ -376,7 +376,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
keyretrieverService := keyretriever.ProvideService(keyRetriever)
|
||||
signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService)
|
||||
localProvider := pluginassets.NewLocalProvider()
|
||||
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider)
|
||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService)
|
||||
unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg)
|
||||
validation := signature.ProvideValidatorService(unsignedPluginAuthorizer)
|
||||
angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore)
|
||||
@@ -714,8 +715,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
|
||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService)
|
||||
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
||||
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
||||
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
||||
@@ -1042,7 +1042,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
keyretrieverService := keyretriever.ProvideService(keyRetriever)
|
||||
signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService)
|
||||
localProvider := pluginassets.NewLocalProvider()
|
||||
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider)
|
||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider, pluginscdnService)
|
||||
unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg)
|
||||
validation := signature.ProvideValidatorService(unsignedPluginAuthorizer)
|
||||
angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore)
|
||||
@@ -1382,8 +1383,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
|
||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
|
||||
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService)
|
||||
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
|
||||
prefService := prefimpl.ProvideService(sqlStore, cfg)
|
||||
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
|
||||
|
||||
@@ -49,7 +49,7 @@ var (
|
||||
Name: "lokiExperimentalStreaming",
|
||||
Description: "Support new streaming approach for loki (prototype, needs special loki build)",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaOSSBigTent,
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
},
|
||||
{
|
||||
Name: "featureHighlights",
|
||||
@@ -177,7 +177,7 @@ var (
|
||||
Name: "lokiLogsDataplane",
|
||||
Description: "Changes logs responses from Loki to be compliant with the dataplane specification.",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaOSSBigTent,
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
},
|
||||
{
|
||||
Name: "disableSSEDataplane",
|
||||
@@ -340,7 +340,7 @@ var (
|
||||
Description: "Enables running Loki queries in parallel",
|
||||
Stage: FeatureStagePrivatePreview,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaOSSBigTent,
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
},
|
||||
{
|
||||
Name: "externalServiceAccounts",
|
||||
@@ -745,7 +745,7 @@ var (
|
||||
Name: "logQLScope",
|
||||
Description: "In-development feature that will allow injection of labels into loki queries.",
|
||||
Stage: FeatureStagePrivatePreview,
|
||||
Owner: grafanaOSSBigTent,
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
Expression: "false",
|
||||
HideFromDocs: true,
|
||||
},
|
||||
@@ -1260,7 +1260,7 @@ var (
|
||||
Name: "lokiLabelNamesQueryApi",
|
||||
Description: "Defaults to using the Loki `/labels` API instead of `/series`",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Owner: grafanaOSSBigTent,
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
Expression: "true",
|
||||
},
|
||||
{
|
||||
|
||||
Generated
+5
-5
@@ -3,7 +3,7 @@ disableEnvelopeEncryption,GA,@grafana/grafana-operator-experience-squad,false,fa
|
||||
panelTitleSearch,preview,@grafana/search-and-storage,false,false,false
|
||||
publicDashboardsEmailSharing,preview,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
publicDashboardsScene,GA,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
lokiExperimentalStreaming,experimental,@grafana/oss-big-tent,false,false,false
|
||||
lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false
|
||||
featureHighlights,GA,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
storage,experimental,@grafana/search-and-storage,false,false,false
|
||||
canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true
|
||||
@@ -22,7 +22,7 @@ starsFromAPIServer,experimental,@grafana/grafana-search-navigate-organise,false,
|
||||
kubernetesStars,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false
|
||||
influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false
|
||||
lokiLogsDataplane,experimental,@grafana/oss-big-tent,false,false,false
|
||||
lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false
|
||||
disableSSEDataplane,experimental,@grafana/grafana-datasources-core-services,false,false,false
|
||||
renderAuthJWT,preview,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false
|
||||
@@ -45,7 +45,7 @@ aiGeneratedDashboardChanges,experimental,@grafana/dashboards-squad,false,false,t
|
||||
reportingRetries,preview,@grafana/grafana-operator-experience-squad,false,true,false
|
||||
reportingCsvEncodingOptions,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
sseGroupByDatasource,experimental,@grafana/grafana-datasources-core-services,false,false,false
|
||||
lokiRunQueriesInParallel,privatePreview,@grafana/oss-big-tent,false,false,false
|
||||
lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false
|
||||
externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false
|
||||
enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
|
||||
disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
|
||||
@@ -102,7 +102,7 @@ alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false
|
||||
scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
useScopeSingleNodeEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
useMultipleScopeNodesEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
logQLScope,privatePreview,@grafana/oss-big-tent,false,false,false
|
||||
logQLScope,privatePreview,@grafana/observability-logs,false,false,false
|
||||
sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false
|
||||
sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true
|
||||
kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
@@ -173,7 +173,7 @@ alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,
|
||||
alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true
|
||||
unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false
|
||||
elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false
|
||||
lokiLabelNamesQueryApi,GA,@grafana/oss-big-tent,false,false,false
|
||||
lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false
|
||||
k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false
|
||||
k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false
|
||||
improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false
|
||||
|
||||
|
+15
-30
@@ -2222,16 +2222,13 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "logQLScope",
|
||||
"resourceVersion": "1768317398145",
|
||||
"creationTimestamp": "2024-11-11T11:53:24Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC"
|
||||
}
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-11-11T11:53:24Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "In-development feature that will allow injection of labels into loki queries.",
|
||||
"stage": "privatePreview",
|
||||
"codeowner": "@grafana/oss-big-tent",
|
||||
"codeowner": "@grafana/observability-logs",
|
||||
"hideFromDocs": true,
|
||||
"expression": "false"
|
||||
}
|
||||
@@ -2307,47 +2304,38 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "lokiExperimentalStreaming",
|
||||
"resourceVersion": "1768317398145",
|
||||
"creationTimestamp": "2023-06-19T10:03:51Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC"
|
||||
}
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2023-06-19T10:03:51Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Support new streaming approach for loki (prototype, needs special loki build)",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/oss-big-tent"
|
||||
"codeowner": "@grafana/observability-logs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "lokiLabelNamesQueryApi",
|
||||
"resourceVersion": "1768317398145",
|
||||
"creationTimestamp": "2024-12-13T14:31:41Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC"
|
||||
}
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-12-13T14:31:41Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Defaults to using the Loki `/labels` API instead of `/series`",
|
||||
"stage": "GA",
|
||||
"codeowner": "@grafana/oss-big-tent",
|
||||
"codeowner": "@grafana/observability-logs",
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "lokiLogsDataplane",
|
||||
"resourceVersion": "1768317398145",
|
||||
"creationTimestamp": "2023-07-13T07:58:00Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC"
|
||||
}
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2023-07-13T07:58:00Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Changes logs responses from Loki to be compliant with the dataplane specification.",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/oss-big-tent"
|
||||
"codeowner": "@grafana/observability-logs"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2380,16 +2368,13 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "lokiRunQueriesInParallel",
|
||||
"resourceVersion": "1768317398145",
|
||||
"creationTimestamp": "2023-09-19T09:34:01Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-13 15:16:38.145488 +0000 UTC"
|
||||
}
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2023-09-19T09:34:01Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables running Loki queries in parallel",
|
||||
"stage": "privatePreview",
|
||||
"codeowner": "@grafana/oss-big-tent"
|
||||
"codeowner": "@grafana/observability-logs"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
|
||||
@@ -213,10 +214,27 @@ func TestLoader_Load(t *testing.T) {
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-app/module.js",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")),
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-app/module.js",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "includes-symlinks")),
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-app",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1622547655175,
|
||||
Files: map[string]string{
|
||||
"dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303",
|
||||
"dashboards/extra/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee",
|
||||
"plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf",
|
||||
"symlink_to_txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048",
|
||||
"text.txt": "9f32c171bf78a85d5cb77a48ab44f85578ee2942a1fc9f9ec4fde194ae4ff048",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
},
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@@ -647,10 +665,24 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
||||
Executable: "test",
|
||||
State: plugins.ReleaseStateAlpha,
|
||||
},
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-datasource/module.js",
|
||||
BaseURL: "public/plugins/test-datasource",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")),
|
||||
Class: plugins.ClassExternal,
|
||||
Module: "public/plugins/test-datasource/module.js",
|
||||
BaseURL: "public/plugins/test-datasource",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature/plugin")),
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-datasource",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1661171417046,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypePrivate,
|
||||
SignedByOrg: "willbrowne",
|
||||
SignedByOrgName: "Will Browne",
|
||||
RootURLs: []string{"http://localhost:3000/"},
|
||||
},
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.SignatureTypePrivate,
|
||||
SignatureOrg: "Will Browne",
|
||||
@@ -767,8 +799,22 @@ func TestLoader_Load_RBACReady(t *testing.T) {
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")),
|
||||
Class: plugins.ClassExternal,
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app-with-roles")),
|
||||
Class: plugins.ClassExternal,
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-app",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1667484928676,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "3348335ec100392b325f3eeb882a07c729e9cbf0f1ae331239f46840bb1a01eb",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypePrivate,
|
||||
SignedByOrg: "gabrielmabille",
|
||||
SignedByOrgName: "gabrielmabille",
|
||||
RootURLs: []string{"http://localhost:3000/"},
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypePrivate,
|
||||
SignatureOrg: "gabrielmabille",
|
||||
@@ -837,8 +883,22 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")),
|
||||
Class: plugins.ClassExternal,
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "valid-v2-pvt-signature-root-url-uri/plugin")),
|
||||
Class: plugins.ClassExternal,
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-datasource",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1661171981629,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "203ef4a613c5693c437a665cd67f95e2756a0f71b336b2ffb265db7c180d0b19",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypePrivate,
|
||||
SignedByOrg: "willbrowne",
|
||||
SignedByOrgName: "Will Browne",
|
||||
RootURLs: []string{"http://localhost:3000/grafana"},
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypePrivate,
|
||||
SignatureOrg: "Will Browne",
|
||||
@@ -925,8 +985,24 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")),
|
||||
Class: plugins.ClassExternal,
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "test-app")),
|
||||
Class: plugins.ClassExternal,
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-app",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1621356785895,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf",
|
||||
"dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303",
|
||||
"dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee",
|
||||
"dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@@ -1017,8 +1093,24 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
FS: mustNewStaticFSForTests(t, pluginDir1),
|
||||
Class: plugins.ClassExternal,
|
||||
FS: mustNewStaticFSForTests(t, pluginDir1),
|
||||
Class: plugins.ClassExternal,
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-app",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1621356785895,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "c59a51bf6d7ecd7a99608ccb99353390c8b973672a938a0247164324005c0caf",
|
||||
"dashboards/connections.json": "bea86da4be970b98dc4681802ab55cdef3441dc3eb3c654cb207948d17b25303",
|
||||
"dashboards/memory.json": "7c042464941084caa91d0a9a2f188b05315a9796308a652ccdee31ca4fbcbfee",
|
||||
"dashboards/connections_result.json": "124d85c9c2e40214b83273f764574937a79909cfac3f925276fbb72543c224dc",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@@ -1180,9 +1272,23 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
Module: "public/plugins/test-datasource/module.js",
|
||||
BaseURL: "public/plugins/test-datasource",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")),
|
||||
Module: "public/plugins/test-datasource/module.js",
|
||||
BaseURL: "public/plugins/test-datasource",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent")),
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-datasource",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1661172777367,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8",
|
||||
"nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@@ -1225,9 +1331,23 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
ExtensionPoints: []plugins.ExtensionPoint{},
|
||||
},
|
||||
},
|
||||
Module: "public/plugins/test-panel/module.js",
|
||||
BaseURL: "public/plugins/test-panel",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")),
|
||||
Module: "public/plugins/test-panel/module.js",
|
||||
BaseURL: "public/plugins/test-panel",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "nested-plugins/parent/nested")),
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "test-datasource",
|
||||
Version: "1.0.0",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1661172777367,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "a029469ace740e9502bfb0d40924d1cccae73d0b18adcd8f1ceb7f17bf36beb8",
|
||||
"nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@@ -1375,10 +1495,25 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
Module: "public/plugins/myorgid-simple-app/module.js",
|
||||
BaseURL: "public/plugins/myorgid-simple-app",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")),
|
||||
DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react",
|
||||
Module: "public/plugins/myorgid-simple-app/module.js",
|
||||
BaseURL: "public/plugins/myorgid-simple-app",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist")),
|
||||
DefaultNavURL: "/plugins/myorgid-simple-app/page/root-page-react",
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "myorgid-simple-app",
|
||||
Version: "%VERSION%",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1642614241713,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e",
|
||||
"child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
RootURLs: []string{},
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@@ -1431,6 +1566,21 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
BaseURL: "public/plugins/myorgid-simple-panel",
|
||||
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "app-with-child/dist/child")),
|
||||
IncludedInAppID: parent.ID,
|
||||
Manifest: &plugins.PluginManifest{
|
||||
Plugin: "myorgid-simple-app",
|
||||
Version: "%VERSION%",
|
||||
KeyID: "7e4d0c6a708866e7",
|
||||
Time: 1642614241713,
|
||||
Files: map[string]string{
|
||||
"plugin.json": "1abecfd0229814f6c284ff3c8dd744548f8d676ab3250cd7902c99dabf11480e",
|
||||
"child/plugin.json": "66ba0dffaf3b1bfa17eb9a8672918fc66d1001f465b1061f4fc19c2f2c100f51",
|
||||
},
|
||||
ManifestVersion: "2.0.0",
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignedByOrg: "grafana",
|
||||
SignedByOrgName: "Grafana Labs",
|
||||
RootURLs: []string{},
|
||||
},
|
||||
Signature: plugins.SignatureStatusValid,
|
||||
SignatureType: plugins.SignatureTypeGrafana,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
@@ -1484,7 +1634,7 @@ func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Servi
|
||||
require.NoError(t, err)
|
||||
|
||||
return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg),
|
||||
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider),
|
||||
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider, pluginscdn.ProvideService(cfg)),
|
||||
pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector),
|
||||
pipeline.ProvideInitializationStage(cfg, reg, backendFactory, proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()),
|
||||
terminate, errTracker)
|
||||
@@ -1514,7 +1664,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loade
|
||||
}
|
||||
|
||||
return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg),
|
||||
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider()),
|
||||
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg)),
|
||||
pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector),
|
||||
pipeline.ProvideInitializationStage(cfg, reg, backendFactoryProvider, proc, authServiceRegistry, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), pluginfakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()),
|
||||
terminate, errTracker)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
|
||||
@@ -42,7 +43,7 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pr registry.Service)
|
||||
})
|
||||
}
|
||||
|
||||
func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider) *bootstrap.Bootstrap {
|
||||
func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider, cdn *pluginscdn.Service) *bootstrap.Bootstrap {
|
||||
disableAlertingForTempoDecorateFunc := func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
|
||||
if p.ID == coreplugin.Tempo && !cfg.Features.TempoAlertingEnabled {
|
||||
p.Alerting = false
|
||||
@@ -52,7 +53,7 @@ func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.Signature
|
||||
|
||||
return bootstrap.New(cfg, bootstrap.Opts{
|
||||
ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, ap),
|
||||
DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg), disableAlertingForTempoDecorateFunc),
|
||||
DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg, cdn), disableAlertingForTempoDecorateFunc),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,12 @@ package pluginassets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
@@ -28,24 +21,20 @@ var (
|
||||
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
|
||||
)
|
||||
|
||||
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, sig *signature.Signature, store pluginstore.Store) *Service {
|
||||
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, store pluginstore.Store) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
cdn: cdn,
|
||||
signature: sig,
|
||||
store: store,
|
||||
log: log.New("pluginassets"),
|
||||
cfg: cfg,
|
||||
cdn: cdn,
|
||||
store: store,
|
||||
log: log.New("pluginassets"),
|
||||
}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
cfg *config.PluginManagementCfg
|
||||
cdn *pluginscdn.Service
|
||||
signature *signature.Signature
|
||||
store pluginstore.Store
|
||||
log log.Logger
|
||||
|
||||
moduleHashCache sync.Map
|
||||
cfg *config.PluginManagementCfg
|
||||
cdn *pluginscdn.Service
|
||||
store pluginstore.Store
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// LoadingStrategy calculates the loading strategy for a plugin.
|
||||
@@ -82,95 +71,6 @@ func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugi
|
||||
return plugins.LoadingStrategyFetch
|
||||
}
|
||||
|
||||
// ModuleHash returns the module.js SHA256 hash for a plugin in the format expected by the browser for SRI checks.
|
||||
// The module hash is read from the plugin's MANIFEST.txt file.
|
||||
// The plugin can also be a nested plugin.
|
||||
// If the plugin is unsigned, an empty string is returned.
|
||||
// The results are cached to avoid repeated reads from the MANIFEST.txt file.
|
||||
func (s *Service) ModuleHash(ctx context.Context, p pluginstore.Plugin) string {
|
||||
k := s.moduleHashCacheKey(p)
|
||||
cachedValue, ok := s.moduleHashCache.Load(k)
|
||||
if ok {
|
||||
return cachedValue.(string)
|
||||
}
|
||||
mh, err := s.moduleHash(ctx, p, "")
|
||||
if err != nil {
|
||||
s.log.Error("Failed to calculate module hash", "plugin", p.ID, "error", err)
|
||||
}
|
||||
s.moduleHashCache.Store(k, mh)
|
||||
return mh
|
||||
}
|
||||
|
||||
// moduleHash is the underlying function for ModuleHash. See its documentation for more information.
|
||||
// If the plugin is not a CDN plugin, the function will return an empty string.
|
||||
// It will read the module hash from the MANIFEST.txt in the [[plugins.FS]] of the provided plugin.
|
||||
// If childFSBase is provided, the function will try to get the hash from MANIFEST.txt for the provided children's
|
||||
// module.js file, rather than for the provided plugin.
|
||||
func (s *Service) moduleHash(ctx context.Context, p pluginstore.Plugin, childFSBase string) (r string, err error) {
|
||||
if !s.cfg.Features.SriChecksEnabled {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Ignore unsigned plugins
|
||||
if !p.Signature.IsValid() {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if p.Parent != nil {
|
||||
// Nested plugin
|
||||
parent, ok := s.store.Plugin(ctx, p.Parent.ID)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("parent plugin plugin %q for child plugin %q not found", p.Parent.ID, p.ID)
|
||||
}
|
||||
|
||||
// The module hash is contained within the parent's MANIFEST.txt file.
|
||||
// For example, the parent's MANIFEST.txt will contain an entry similar to this:
|
||||
//
|
||||
// ```
|
||||
// "datasource/module.js": "1234567890abcdef..."
|
||||
// ```
|
||||
//
|
||||
// Recursively call moduleHash with the parent plugin and with the children plugin folder path
|
||||
// to get the correct module hash for the nested plugin.
|
||||
if childFSBase == "" {
|
||||
childFSBase = p.Base()
|
||||
}
|
||||
return s.moduleHash(ctx, parent, childFSBase)
|
||||
}
|
||||
|
||||
// Only CDN plugins are supported for SRI checks.
|
||||
// CDN plugins have the version as part of the URL, which acts as a cache-buster.
|
||||
// Needed due to: https://github.com/grafana/plugin-tools/pull/1426
|
||||
// FS plugins build before this change will have SRI mismatch issues.
|
||||
if !s.cdnEnabled(p.ID, p.FS) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
manifest, err := s.signature.ReadPluginManifestFromFS(ctx, p.FS)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read plugin manifest: %w", err)
|
||||
}
|
||||
if !manifest.IsV2() {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var childPath string
|
||||
if childFSBase != "" {
|
||||
// Calculate the relative path of the child plugin folder from the parent plugin folder.
|
||||
childPath, err = p.FS.Rel(childFSBase)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("rel path: %w", err)
|
||||
}
|
||||
// MANIFETS.txt uses forward slashes as path separators.
|
||||
childPath = filepath.ToSlash(childPath)
|
||||
}
|
||||
moduleHash, ok := manifest.Files[path.Join(childPath, "module.js")]
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
return convertHashForSRI(moduleHash)
|
||||
}
|
||||
|
||||
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
||||
if cpv, ok := ps[CreatePluginVersionCfgKey]; ok {
|
||||
createPluginVer, err := semver.NewVersion(cpv)
|
||||
@@ -188,17 +88,3 @@ func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
|
||||
func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool {
|
||||
return s.cdn.PluginSupported(pluginID) || fs.Type().CDN()
|
||||
}
|
||||
|
||||
// convertHashForSRI takes a SHA256 hash string and returns it as expected by the browser for SRI checks.
|
||||
func convertHashForSRI(h string) (string, error) {
|
||||
hb, err := hex.DecodeString(h)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("hex decode string: %w", err)
|
||||
}
|
||||
return "sha256-" + base64.StdEncoding.EncodeToString(hb), nil
|
||||
}
|
||||
|
||||
// moduleHashCacheKey returns a unique key for the module hash cache.
|
||||
func (s *Service) moduleHashCacheKey(p pluginstore.Plugin) string {
|
||||
return p.ID + ":" + p.Info.Version
|
||||
}
|
||||
|
||||
@@ -2,19 +2,14 @@ package pluginassets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
@@ -179,349 +174,6 @@ func TestService_Calculate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ModuleHash(t *testing.T) {
|
||||
const (
|
||||
pluginID = "grafana-test-datasource"
|
||||
parentPluginID = "grafana-test-app"
|
||||
)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
features *config.Features
|
||||
store []pluginstore.Plugin
|
||||
|
||||
// Can be used to configure plugin's fs
|
||||
// fs cdn type = loaded from CDN with no files on disk
|
||||
// fs local type = files on disk but served from CDN only if cdn=true
|
||||
plugin pluginstore.Plugin
|
||||
|
||||
// When true, set cdn=true in config
|
||||
cdn bool
|
||||
expModuleHash string
|
||||
}{
|
||||
{
|
||||
name: "unsigned should not return module hash",
|
||||
plugin: newPlugin(pluginID, withSignatureStatus(plugins.SignatureStatusUnsigned)),
|
||||
cdn: false,
|
||||
features: &config.Features{SriChecksEnabled: false},
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
withClass(plugins.ClassExternal),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
||||
},
|
||||
{
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
withClass(plugins.ClassExternal),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"),
|
||||
},
|
||||
{
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
),
|
||||
cdn: false,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: false},
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
),
|
||||
cdn: false,
|
||||
features: &config.Features{SriChecksEnabled: false},
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
// parentPluginID (/)
|
||||
// └── pluginID (/datasource)
|
||||
name: "nested plugin should return module hash from parent MANIFEST.txt",
|
||||
store: []pluginstore.Plugin{
|
||||
newPlugin(
|
||||
parentPluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))),
|
||||
),
|
||||
},
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "datasource"))),
|
||||
withParent(parentPluginID),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: newSRIHash(t, "04d70db091d96c4775fb32ba5a8f84cc22893eb43afdb649726661d4425c6711"),
|
||||
},
|
||||
{
|
||||
// parentPluginID (/)
|
||||
// └── pluginID (/panels/one)
|
||||
name: "nested plugin deeper than one subfolder should return module hash from parent MANIFEST.txt",
|
||||
store: []pluginstore.Plugin{
|
||||
newPlugin(
|
||||
parentPluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))),
|
||||
),
|
||||
},
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))),
|
||||
withParent(parentPluginID),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"),
|
||||
},
|
||||
{
|
||||
// grand-parent-app (/)
|
||||
// ├── parent-datasource (/datasource)
|
||||
// │ └── child-panel (/datasource/panels/one)
|
||||
name: "nested plugin of a nested plugin should return module hash from parent MANIFEST.txt",
|
||||
store: []pluginstore.Plugin{
|
||||
newPlugin(
|
||||
"grand-parent-app",
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested"))),
|
||||
),
|
||||
newPlugin(
|
||||
"parent-datasource",
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource"))),
|
||||
withParent("grand-parent-app"),
|
||||
),
|
||||
},
|
||||
plugin: newPlugin(
|
||||
"child-panel",
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-deeply-nested", "datasource", "panels", "one"))),
|
||||
withParent("parent-datasource"),
|
||||
),
|
||||
cdn: true,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: newSRIHash(t, "cbd1ac2284645a0e1e9a8722a729f5bcdd2b831222728709c6360beecdd6143f"),
|
||||
},
|
||||
{
|
||||
name: "nested plugin should not return module hash from parent if it's not registered in the store",
|
||||
store: []pluginstore.Plugin{},
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested", "panels", "one"))),
|
||||
withParent(parentPluginID),
|
||||
),
|
||||
cdn: false,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "missing module.js entry from MANIFEST.txt should not return module hash",
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-module-js"))),
|
||||
),
|
||||
cdn: false,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: "",
|
||||
},
|
||||
{
|
||||
name: "signed status but missing MANIFEST.txt should not return module hash",
|
||||
plugin: newPlugin(
|
||||
pluginID,
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-no-manifest-txt"))),
|
||||
),
|
||||
cdn: false,
|
||||
features: &config.Features{SriChecksEnabled: true},
|
||||
expModuleHash: "",
|
||||
},
|
||||
} {
|
||||
if tc.name == "" {
|
||||
var expS string
|
||||
if tc.expModuleHash == "" {
|
||||
expS = "should not return module hash"
|
||||
} else {
|
||||
expS = "should return module hash"
|
||||
}
|
||||
tc.name = fmt.Sprintf("feature=%v, cdn_config=%v, class=%v %s", tc.features.SriChecksEnabled, tc.cdn, tc.plugin.Class, expS)
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var pluginSettings config.PluginSettings
|
||||
if tc.cdn {
|
||||
pluginSettings = config.PluginSettings{
|
||||
pluginID: {
|
||||
"cdn": "true",
|
||||
},
|
||||
parentPluginID: map[string]string{
|
||||
"cdn": "true",
|
||||
},
|
||||
"grand-parent-app": map[string]string{
|
||||
"cdn": "true",
|
||||
},
|
||||
}
|
||||
}
|
||||
features := tc.features
|
||||
if features == nil {
|
||||
features = &config.Features{}
|
||||
}
|
||||
pCfg := &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "http://cdn.example.com",
|
||||
PluginSettings: pluginSettings,
|
||||
Features: *features,
|
||||
}
|
||||
svc := ProvideService(
|
||||
pCfg,
|
||||
pluginscdn.ProvideService(pCfg),
|
||||
signature.ProvideService(pCfg, statickey.New()),
|
||||
pluginstore.NewFakePluginStore(tc.store...),
|
||||
)
|
||||
mh := svc.ModuleHash(context.Background(), tc.plugin)
|
||||
require.Equal(t, tc.expModuleHash, mh)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ModuleHash_Cache(t *testing.T) {
|
||||
pCfg := &config.PluginManagementCfg{
|
||||
PluginSettings: config.PluginSettings{},
|
||||
Features: config.Features{SriChecksEnabled: true},
|
||||
}
|
||||
svc := ProvideService(
|
||||
pCfg,
|
||||
pluginscdn.ProvideService(pCfg),
|
||||
signature.ProvideService(pCfg, statickey.New()),
|
||||
pluginstore.NewFakePluginStore(),
|
||||
)
|
||||
const pluginID = "grafana-test-datasource"
|
||||
|
||||
t.Run("cache key", func(t *testing.T) {
|
||||
t.Run("with version", func(t *testing.T) {
|
||||
const pluginVersion = "1.0.0"
|
||||
p := newPlugin(pluginID, withInfo(plugins.Info{Version: pluginVersion}))
|
||||
k := svc.moduleHashCacheKey(p)
|
||||
require.Equal(t, pluginID+":"+pluginVersion, k, "cache key should be correct")
|
||||
})
|
||||
|
||||
t.Run("without version", func(t *testing.T) {
|
||||
p := newPlugin(pluginID)
|
||||
k := svc.moduleHashCacheKey(p)
|
||||
require.Equal(t, pluginID+":", k, "cache key should be correct")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ModuleHash usage", func(t *testing.T) {
|
||||
pV1 := newPlugin(
|
||||
pluginID,
|
||||
withInfo(plugins.Info{Version: "1.0.0"}),
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid"))),
|
||||
)
|
||||
|
||||
pCfg = &config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "https://cdn.grafana.com",
|
||||
PluginSettings: config.PluginSettings{
|
||||
pluginID: {
|
||||
"cdn": "true",
|
||||
},
|
||||
},
|
||||
Features: config.Features{SriChecksEnabled: true},
|
||||
}
|
||||
svc = ProvideService(
|
||||
pCfg,
|
||||
pluginscdn.ProvideService(pCfg),
|
||||
signature.ProvideService(pCfg, statickey.New()),
|
||||
pluginstore.NewFakePluginStore(),
|
||||
)
|
||||
|
||||
k := svc.moduleHashCacheKey(pV1)
|
||||
|
||||
_, ok := svc.moduleHashCache.Load(k)
|
||||
require.False(t, ok, "cache should initially be empty")
|
||||
|
||||
mhV1 := svc.ModuleHash(context.Background(), pV1)
|
||||
pV1Exp := newSRIHash(t, "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03")
|
||||
require.Equal(t, pV1Exp, mhV1, "returned value should be correct")
|
||||
|
||||
cachedMh, ok := svc.moduleHashCache.Load(k)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, pV1Exp, cachedMh, "cache should contain the returned value")
|
||||
|
||||
t.Run("different version uses different cache key", func(t *testing.T) {
|
||||
pV2 := newPlugin(
|
||||
pluginID,
|
||||
withInfo(plugins.Info{Version: "2.0.0"}),
|
||||
withSignatureStatus(plugins.SignatureStatusValid),
|
||||
// different fs for different hash
|
||||
withFS(plugins.NewLocalFS(filepath.Join("testdata", "module-hash-valid-nested"))),
|
||||
)
|
||||
mhV2 := svc.ModuleHash(context.Background(), pV2)
|
||||
require.NotEqual(t, mhV2, mhV1, "different version should have different hash")
|
||||
require.Equal(t, newSRIHash(t, "266c19bc148b22ddef2a288fc5f8f40855bda22ccf60be53340b4931e469ae2a"), mhV2)
|
||||
})
|
||||
|
||||
t.Run("cache should be used", func(t *testing.T) {
|
||||
// edit cache directly
|
||||
svc.moduleHashCache.Store(k, "hax")
|
||||
require.Equal(t, "hax", svc.ModuleHash(context.Background(), pV1))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertHashFromSRI(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
hash string
|
||||
expHash string
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
hash: "ddfcb449445064e6c39f0c20b15be3cb6a55837cf4781df23d02de005f436811",
|
||||
expHash: "sha256-3fy0SURQZObDnwwgsVvjy2pVg3z0eB3yPQLeAF9DaBE=",
|
||||
},
|
||||
{
|
||||
hash: "not-a-valid-hash",
|
||||
expErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.hash, func(t *testing.T) {
|
||||
r, err := convertHashForSRI(tc.hash)
|
||||
if tc.expErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expHash, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
|
||||
p := pluginstore.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
@@ -534,13 +186,6 @@ func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Pl
|
||||
return p
|
||||
}
|
||||
|
||||
func withInfo(info plugins.Info) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Info = info
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.FS = fs
|
||||
@@ -548,13 +193,6 @@ func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
func withSignatureStatus(status plugins.SignatureStatus) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Signature = status
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Angular = plugins.AngularMeta{Detected: angular}
|
||||
@@ -562,13 +200,6 @@ func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
func withParent(parentID string) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: parentID}
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Class = class
|
||||
@@ -587,9 +218,3 @@ func newPluginSettings(pluginID string, kv map[string]string) config.PluginSetti
|
||||
pluginID: kv,
|
||||
}
|
||||
}
|
||||
|
||||
func newSRIHash(t *testing.T, s string) string {
|
||||
r, err := convertHashForSRI(s)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@ type Plugin struct {
|
||||
Error *plugins.Error
|
||||
|
||||
// SystemJS fields
|
||||
Module string
|
||||
BaseURL string
|
||||
Module string
|
||||
BaseURL string
|
||||
ModuleHash string
|
||||
|
||||
Angular plugins.AngularMeta
|
||||
|
||||
@@ -80,6 +81,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
|
||||
ExternalService: p.ExternalService,
|
||||
Angular: p.Angular,
|
||||
Translations: p.Translations,
|
||||
ModuleHash: p.ModuleHash,
|
||||
}
|
||||
|
||||
if p.Parent != nil {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginerrs"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/coreplugin"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
||||
@@ -49,7 +50,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core
|
||||
proc := process.ProvideService()
|
||||
|
||||
disc := pipeline.ProvideDiscoveryStage(pCfg, reg)
|
||||
boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider())
|
||||
boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(pCfg))
|
||||
valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector)
|
||||
init := pipeline.ProvideInitializationStage(pCfg, reg, coreplugin.ProvideCoreProvider(coreRegistry), proc, &pluginfakes.FakeAuthService{}, pluginfakes.NewFakeRoleRegistry(), pluginfakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop())
|
||||
term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc)
|
||||
@@ -87,7 +88,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts Lo
|
||||
}
|
||||
|
||||
if opts.Bootstrapper == nil {
|
||||
opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider())
|
||||
opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider(), pluginscdn.ProvideService(cfg))
|
||||
}
|
||||
|
||||
if opts.Validator == nil {
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@ describe('isDashboardDatasource', () => {
|
||||
{ refId: 'C', datasource: { uid: 'mysql-uid', type: 'mysql' } },
|
||||
];
|
||||
|
||||
const backendQueries = queries.filter((q) => !isDashboardDatasource(q));
|
||||
const nonDashboardQueries = queries.filter((q) => !isDashboardDatasource(q));
|
||||
|
||||
expect(backendQueries.map((q) => q.refId)).toEqual(['A', 'C']);
|
||||
expect(nonDashboardQueries.map((q) => q.refId)).toEqual(['A', 'C']);
|
||||
});
|
||||
|
||||
it('returns true when query has dashboard datasource uid', () => {
|
||||
|
||||
@@ -67,9 +67,9 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
|
||||
|
||||
try {
|
||||
// Filter out Dashboard datasource queries - they are frontend-only and can't be processed by backend
|
||||
const backendQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
|
||||
const nonDashboardQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
|
||||
|
||||
if (backendQueries.length === 0) {
|
||||
if (nonDashboardQueries.length === 0) {
|
||||
setSchemas({ kind: 'SQLSchemaResponse', apiVersion: 'query.grafana.app/v0alpha1', sqlSchemas: {} });
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -81,7 +81,7 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
|
||||
const response = await getBackendSrv().post<SQLSchemasResponse>(
|
||||
`/apis/query.grafana.app/v0alpha1/namespaces/${namespace}/sqlschemas/name`,
|
||||
{
|
||||
queries: backendQueries,
|
||||
queries: nonDashboardQueries,
|
||||
from: currentTimeRange.from.toISOString(),
|
||||
to: currentTimeRange.to.toISOString(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user