Compare commits

..

2 Commits

Author SHA1 Message Date
Oscar Kilhed
f209249f68 V2 Schema + DD: Patch fast with dashboard fixes related to V2 Schema and dynamic dashboards. (#115620)
* V2: Fix ad hoc filter defaultKeys incorrectly set to static mode (#115508)

* V2: Fix ad hoc filter defaultKeys incorrectly set to static mode

* Fixture update

* Dashboard Schema v1beta1 to v2alpha1: Preserve string template variable datasource references in query variables (#115516)

* Dashboard migration: preserve legacy string datasource references

Fix v1beta1 → v2alpha1 conversion to handle legacy string datasource
references in QueryVariable, AdhocVariable, and GroupByVariable.

Previously, string datasource references (both template variables like
"$datasource" and direct names/UIDs like "prometheus") were being
dropped during conversion, causing variable chaining to break.

The frontend's DatasourceSrv.getInstanceSettings() already handles
string references by trying uid → name → id lookup at runtime, so we
preserve the string in the uid field and let the frontend resolve it.

* trigger frontend ci tests when dashboard migration code changes

* v1: if string convert to DS ref

* Update migration testdata to fix template variable datasource references

* update

* Schema V2: Always set unique refid for queries in conversion V1 -> V2 (#115534)

Always set unique refid in conversion

* Dashboards: Fix text panel content loss during v1 to v2 migration (#115496)

* move content and mode properties to options level

* move to angular section

* Update comments

* handle missing angular text panel

* re-generate test files

* angualr panels tests

* fixing test

* Update output files

* Update output for dev dashboard

* Spread options at the top panel level for migration

* linting issue

---------

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>

* V2 -> V1 conversion: include empty properties array when converting overrides (#115495)

* adjust conversion file to include empty properties array in overrides

* fix lint error

* add test case for empty properties and fix incorrect regex to v1 conversion

* Dashboard: change export dropdown placement in sidebar (#115515)

Update export menu placement

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com>
Co-authored-by: Kristina Demeshchik <kristina.demeshchik@gmail.com>
Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
Co-authored-by: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com>
2025-12-19 14:56:49 +01:00
Laura Fernández
199cbe0e18 Rudderstack: Add new config option for rudderstack v3 url (#115374) 2025-12-18 17:46:34 -03:00
696 changed files with 13336 additions and 31174 deletions

2
.github/CODEOWNERS vendored
View File

@@ -24,6 +24,7 @@
/NOTICE.md @torkelo
/README.md @grafana/docs-grafana
/ROADMAP.md @torkelo
/SECURITY.md @grafana/security-team
/SUPPORT.md @torkelo
/WORKFLOW.md @torkelo
/contribute/ @grafana/grafana-community-support
@@ -425,7 +426,6 @@ i18next.config.ts @grafana/grafana-frontend-platform
/public/locales/enterprise/i18next.config.ts @grafana/grafana-frontend-platform
/public/app/core/internationalization/ @grafana/grafana-frontend-platform
/e2e/ @grafana/grafana-frontend-platform
/e2e-playwright/alerting-suite/ @grafana/alerting-frontend
/e2e-playwright/cloud-plugins-suite/ @grafana/partner-datasources
/e2e-playwright/dashboard-new-layouts/ @grafana/dashboards-squad
/e2e-playwright/dashboard-cujs/ @grafana/dashboards-squad

View File

@@ -82,6 +82,14 @@ inputs:
description: Docker registry of produced images
default: docker.io
required: false
ubuntu-base:
type: string
default: 'ubuntu:22.04'
required: false
alpine-base:
type: string
default: 'alpine:3.22'
required: false
outputs:
dist-dir:
description: Directory where artifacts are placed
@@ -126,11 +134,13 @@ runs:
UBUNTU_TAG_FORMAT: ${{ inputs.docker-tag-format-ubuntu }}
CHECKSUM: ${{ inputs.checksum }}
VERIFY: ${{ inputs.verify }}
ALPINE_BASE: ${{ inputs.alpine-base }}
UBUNTU_BASE: ${{ inputs.ubuntu-base }}
with:
verb: run
dagger-flags: --verbose=0
version: 0.18.8
args: go run -C ${GRAFANA_PATH} ./pkg/build/cmd artifacts --artifacts ${ARTIFACTS} --grafana-dir=${GRAFANA_PATH} --enterprise-dir=${ENTERPRISE_PATH} --version=${VERSION} --patches-repo=${PATCHES_REPO} --patches-ref=${PATCHES_REF} --patches-path=${PATCHES_PATH} --build-id=${BUILD_ID} --tag-format="${TAG_FORMAT}" --ubuntu-tag-format="${UBUNTU_TAG_FORMAT}" --org=${DOCKER_ORG} --registry=${DOCKER_REGISTRY} --checksum=${CHECKSUM} --verify=${VERIFY} > $OUTFILE
args: go run -C ${GRAFANA_PATH} ./pkg/build/cmd artifacts --artifacts ${ARTIFACTS} --grafana-dir=${GRAFANA_PATH} --alpine-base=${ALPINE_BASE} --ubuntu-base=${UBUNTU_BASE} --enterprise-dir=${ENTERPRISE_PATH} --version=${VERSION} --patches-repo=${PATCHES_REPO} --patches-ref=${PATCHES_REF} --patches-path=${PATCHES_PATH} --build-id=${BUILD_ID} --tag-format="${TAG_FORMAT}" --ubuntu-tag-format="${UBUNTU_TAG_FORMAT}" --org=${DOCKER_ORG} --registry=${DOCKER_REGISTRY} --checksum=${CHECKSUM} --verify=${VERIFY} > $OUTFILE
- id: output
shell: bash
env:

View File

@@ -99,7 +99,6 @@ runs:
- '${{ inputs.self }}'
e2e:
- 'e2e/**'
- 'e2e-playwright/**'
- '.github/actions/setup-enterprise/**'
- '.github/actions/checkout/**'
- 'emails/**'

View File

@@ -365,9 +365,7 @@
"type": "changedfiles",
"matches": [
"public/app/plugins/panel/gauge/**/*",
"public/app/plugins/panel/radialbar/**/*",
"/packages/grafana-ui/src/components/Gauge/**/*",
"/packages/grafana-ui/src/components/RadialGauge/**/*"
"/packages/grafana-ui/src/components/Gauge/**/*"
],
"action": "updateLabel",
"addLabel": "area/panel/gauge"

View File

@@ -14,5 +14,5 @@ jobs:
- uses: actions/checkout@v5
- uses: grafana/shared-workflows/actions/cleanup-branches@cleanup-branches/v0.2.1
with:
dry-run: false
dry-run: true
max-date: "1 month ago"

View File

@@ -1,13 +0,0 @@
diff --git a/dist/builder-manager/index.js b/dist/builder-manager/index.js
index 3d7f9b213dae1801bda62b31db31b9113e382ccd..212501c63d20146c29db63fb0f6300c6779eecb5 100644
--- a/dist/builder-manager/index.js
+++ b/dist/builder-manager/index.js
@@ -1970,7 +1970,7 @@ var pa = /^\/($|\?)/, G, C, xt = /* @__PURE__ */ o(async (e) => {
bundle: !0,
minify: !0,
sourcemap: !1,
- conditions: ["browser", "module", "default"],
+ conditions: ["@grafana-app/source", "browser", "module", "default"],
jsxFactory: "React.createElement",
jsxFragment: "React.Fragment",
jsx: "transform",

29
SECURITY.md Normal file
View File

@@ -0,0 +1,29 @@
# Reporting security issues
If you think you have found a security vulnerability, we have two routes for reporting security issues.
Important: Whichever route you choose, we ask you to not disclose the vulnerability before it has been fixed and announced, unless you received a response from the Grafana Labs security team that you can do so.
[Full guidance on reporting a security issue can be found here](https://grafana.com/legal/report-a-security-issue/).
This product is in scope for our Bug Bounty Program. To submit a vulnerability report, please visit [Grafana Labs Bug Bounty page](https://app.intigriti.com/programs/grafanalabs/grafanaossbbp/detail) and follow the instructions provided. Our security team will review your submission and get back to you as soon as possible.
---
For products and services outside the scope of our bug bounty program, or if you do not wish to receive a bounty, you can report issues directly to us via email at security@grafana.com. This address can be used for all of Grafana Labs open source and commercial products (including but not limited to Grafana, Grafana Cloud, Grafana Enterprise, and grafana.com).
Please encrypt your message to us; please use our PGP key. The key fingerprint is:
225E 6A9B BB15 A37E 95EB 6312 C66A 51CC B44C 27E0
The key is available from [keyserver.ubuntu.com](https://keyserver.ubuntu.com/pks/lookup?search=0x225E6A9BBB15A37E95EB6312C66A51CCB44C27E0&fingerprint=on&op=index).
Grafana Labs will send you a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
**Important:** We ask you to not disclose the vulnerability before it have been fixed and announced, unless you received a response from the Grafana Labs security team that you can do so.
## Security announcements
We will post a summary, remediation, and mitigation details for any patch containing security fixes on the Grafana blog. The security announcement blog posts will be tagged with the [security tag](https://grafana.com/tags/security/).
You can also track security announcements via the [RSS feed](https://grafana.com/tags/security/index.xml).

View File

@@ -157,7 +157,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/wire v0.7.0 // indirect
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 // indirect
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7 // indirect
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
@@ -165,7 +165,6 @@ require (
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grafana/sqlds/v4 v4.2.7 // indirect

View File

@@ -619,8 +619,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7 h1:ZzG/gCclEit9w0QUfQt9GURcOycAIGcsQAhY1u0AEX0=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=

View File

@@ -4,7 +4,7 @@ go 1.25.5
require (
github.com/go-kit/log v0.2.1
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
github.com/grafana/grafana-app-sdk v0.48.7
github.com/grafana/grafana-app-sdk/logging v0.48.7

View File

@@ -243,8 +243,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7 h1:ZzG/gCclEit9w0QUfQt9GURcOycAIGcsQAhY1u0AEX0=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=

View File

@@ -31,10 +31,6 @@ const (
maxLimit = 1000
Namespace = "grafana"
Subsystem = "alerting"
// LogQL field path for alert rule UID after JSON parsing.
// Loki flattens nested JSON fields with underscores: alert.labels.__alert_rule_uid__ -> alert_labels___alert_rule_uid__
lokiAlertRuleUIDField = "alert_labels___alert_rule_uid__"
)
var (
@@ -115,13 +111,13 @@ func buildQuery(query Query) (string, error) {
fmt.Sprintf(`%s=%q`, historian.LabelFrom, historian.LabelFromValue),
}
logql := fmt.Sprintf(`{%s} | json`, strings.Join(selectors, `,`))
// Add ruleUID filter as JSON line filter if specified.
if query.RuleUID != nil && *query.RuleUID != "" {
logql += fmt.Sprintf(` | %s = %q`, lokiAlertRuleUIDField, *query.RuleUID)
if query.RuleUID != nil {
selectors = append(selectors,
fmt.Sprintf(`%s=%q`, historian.LabelRuleUID, *query.RuleUID))
}
logql := fmt.Sprintf(`{%s} | json`, strings.Join(selectors, `,`))
// Add receiver filter if specified.
if query.Receiver != nil && *query.Receiver != "" {
logql += fmt.Sprintf(` | receiver = %q`, *query.Receiver)
@@ -215,13 +211,16 @@ func parseLokiEntry(s lokiclient.Sample) (Entry, error) {
groupLabels = make(map[string]string)
}
alerts := []EntryAlert{{
Status: lokiEntry.Alert.Status,
Labels: lokiEntry.Alert.Labels,
Annotations: lokiEntry.Alert.Annotations,
StartsAt: lokiEntry.Alert.StartsAt,
EndsAt: lokiEntry.Alert.EndsAt,
}}
alerts := make([]EntryAlert, len(lokiEntry.Alerts))
for i, a := range lokiEntry.Alerts {
alerts[i] = EntryAlert{
Status: a.Status,
Labels: a.Labels,
Annotations: a.Annotations,
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,
}
}
return Entry{
Timestamp: s.T,

View File

@@ -7,7 +7,6 @@ import (
"testing"
"time"
"github.com/grafana/alerting/models"
"github.com/grafana/alerting/notify/historian"
"github.com/grafana/alerting/notify/historian/lokiclient"
"github.com/grafana/grafana-app-sdk/logging"
@@ -134,8 +133,9 @@ func TestBuildQuery(t *testing.T) {
query: Query{
RuleUID: stringPtr("test-rule-uid"),
},
expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid"`,
historian.LabelFrom, historian.LabelFromValue),
expected: fmt.Sprintf(`{%s=%q,%s=%q} | json`,
historian.LabelFrom, historian.LabelFromValue,
historian.LabelRuleUID, "test-rule-uid"),
},
{
name: "query with receiver filter",
@@ -143,8 +143,9 @@ func TestBuildQuery(t *testing.T) {
RuleUID: stringPtr("test-rule-uid"),
Receiver: stringPtr("email-receiver"),
},
expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | receiver = "email-receiver"`,
historian.LabelFrom, historian.LabelFromValue),
expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | receiver = "email-receiver"`,
historian.LabelFrom, historian.LabelFromValue,
historian.LabelRuleUID, "test-rule-uid"),
},
{
name: "query with status filter",
@@ -152,8 +153,9 @@ func TestBuildQuery(t *testing.T) {
RuleUID: stringPtr("test-rule-uid"),
Status: createStatusPtr(v0alpha1.CreateNotificationqueryRequestNotificationStatusFiring),
},
expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | status = "firing"`,
historian.LabelFrom, historian.LabelFromValue),
expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | status = "firing"`,
historian.LabelFrom, historian.LabelFromValue,
historian.LabelRuleUID, "test-rule-uid"),
},
{
name: "query with success outcome filter",
@@ -161,8 +163,9 @@ func TestBuildQuery(t *testing.T) {
RuleUID: stringPtr("test-rule-uid"),
Outcome: outcomePtr(v0alpha1.CreateNotificationqueryRequestNotificationOutcomeSuccess),
},
expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | error = ""`,
historian.LabelFrom, historian.LabelFromValue),
expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | error = ""`,
historian.LabelFrom, historian.LabelFromValue,
historian.LabelRuleUID, "test-rule-uid"),
},
{
name: "query with error outcome filter",
@@ -170,8 +173,9 @@ func TestBuildQuery(t *testing.T) {
RuleUID: stringPtr("test-rule-uid"),
Outcome: outcomePtr(v0alpha1.CreateNotificationqueryRequestNotificationOutcomeError),
},
expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | error != ""`,
historian.LabelFrom, historian.LabelFromValue),
expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | error != ""`,
historian.LabelFrom, historian.LabelFromValue,
historian.LabelRuleUID, "test-rule-uid"),
},
{
name: "query with many filters",
@@ -181,8 +185,9 @@ func TestBuildQuery(t *testing.T) {
Status: createStatusPtr(v0alpha1.CreateNotificationqueryRequestNotificationStatusResolved),
Outcome: outcomePtr(v0alpha1.CreateNotificationqueryRequestNotificationOutcomeSuccess),
},
expected: fmt.Sprintf(`{%s=%q} | json | alert_labels___alert_rule_uid__ = "test-rule-uid" | receiver = "email-receiver" | status = "resolved" | error = ""`,
historian.LabelFrom, historian.LabelFromValue),
expected: fmt.Sprintf(`{%s=%q,%s=%q} | json | receiver = "email-receiver" | status = "resolved" | error = ""`,
historian.LabelFrom, historian.LabelFromValue,
historian.LabelRuleUID, "test-rule-uid"),
},
{
name: "query with group label matcher",
@@ -272,19 +277,19 @@ func TestParseLokiEntry(t *testing.T) {
GroupLabels: map[string]string{
"alertname": "test-alert",
},
Alert: historian.NotificationHistoryLokiEntryAlert{
Status: "firing",
Labels: map[string]string{
"severity": "critical",
Alerts: []historian.NotificationHistoryLokiEntryAlert{
{
Status: "firing",
Labels: map[string]string{
"severity": "critical",
},
Annotations: map[string]string{
"summary": "Test alert",
},
StartsAt: now,
EndsAt: now.Add(1 * time.Hour),
},
Annotations: map[string]string{
"summary": "Test alert",
},
StartsAt: now,
EndsAt: now.Add(1 * time.Hour),
},
AlertIndex: 0,
AlertCount: 1,
Retry: false,
Duration: 100,
PipelineTime: now,
@@ -330,9 +335,7 @@ func TestParseLokiEntry(t *testing.T) {
Error: "notification failed",
GroupKey: "key:thing",
GroupLabels: map[string]string{},
Alert: historian.NotificationHistoryLokiEntryAlert{},
AlertIndex: 0,
AlertCount: 1,
Alerts: []historian.NotificationHistoryLokiEntryAlert{},
PipelineTime: now,
}),
},
@@ -344,7 +347,7 @@ func TestParseLokiEntry(t *testing.T) {
Outcome: OutcomeError,
GroupKey: "key:thing",
GroupLabels: map[string]string{},
Alerts: []EntryAlert{{}},
Alerts: []EntryAlert{},
Error: stringPtr("notification failed"),
PipelineTime: now,
},
@@ -362,7 +365,7 @@ func TestParseLokiEntry(t *testing.T) {
Status: Status("firing"),
Outcome: OutcomeSuccess,
GroupLabels: map[string]string{},
Alerts: []EntryAlert{{}},
Alerts: []EntryAlert{},
PipelineTime: now,
},
},
@@ -445,9 +448,7 @@ func TestLokiReader_RunQuery(t *testing.T) {
Receiver: "receiver-1",
Status: "firing",
GroupLabels: map[string]string{},
Alert: historian.NotificationHistoryLokiEntryAlert{},
AlertIndex: 0,
AlertCount: 1,
Alerts: []historian.NotificationHistoryLokiEntryAlert{},
PipelineTime: now,
}),
},
@@ -458,9 +459,7 @@ func TestLokiReader_RunQuery(t *testing.T) {
Receiver: "receiver-3",
Status: "firing",
GroupLabels: map[string]string{},
Alert: historian.NotificationHistoryLokiEntryAlert{},
AlertIndex: 0,
AlertCount: 1,
Alerts: []historian.NotificationHistoryLokiEntryAlert{},
PipelineTime: now,
}),
},
@@ -475,9 +474,7 @@ func TestLokiReader_RunQuery(t *testing.T) {
Receiver: "receiver-2",
Status: "firing",
GroupLabels: map[string]string{},
Alert: historian.NotificationHistoryLokiEntryAlert{},
AlertIndex: 0,
AlertCount: 1,
Alerts: []historian.NotificationHistoryLokiEntryAlert{},
PipelineTime: now,
}),
},
@@ -549,19 +546,19 @@ func createMockLokiResponse(timestamp time.Time) lokiclient.QueryRes {
GroupLabels: map[string]string{
"alertname": "test-alert",
},
Alert: historian.NotificationHistoryLokiEntryAlert{
Status: "firing",
Labels: map[string]string{
"severity": "critical",
Alerts: []historian.NotificationHistoryLokiEntryAlert{
{
Status: "firing",
Labels: map[string]string{
"severity": "critical",
},
Annotations: map[string]string{
"summary": "Test alert",
},
StartsAt: timestamp,
EndsAt: timestamp.Add(1 * time.Hour),
},
Annotations: map[string]string{
"summary": "Test alert",
},
StartsAt: timestamp,
EndsAt: timestamp.Add(1 * time.Hour),
},
AlertIndex: 0,
AlertCount: 1,
Retry: false,
Duration: 100,
PipelineTime: timestamp,
@@ -590,19 +587,10 @@ func createLokiEntryJSONWithNilLabels(t *testing.T, timestamp time.Time) string
"status": "firing",
"error": "",
"groupLabels": null,
"alert": {},
"alertIndex": 0,
"alertCount": 1,
"alerts": [],
"retry": false,
"duration": 0,
"pipelineTime": "%s"
}`, timestamp.Format(time.RFC3339Nano))
return jsonStr
}
func TestRuleUIDLabelConstant(t *testing.T) {
// Verify that models.RuleUIDLabel has the expected value.
// If this changes in the alerting module, our LogQL field path constant will be incorrect
// and filtering for a single alert rule by its UID will break.
assert.Equal(t, "__alert_rule_uid__", models.RuleUIDLabel)
}

View File

@@ -1,9 +1,6 @@
package v0alpha1
TemplateKind: *"grafana" | "mimir"
TemplateGroupSpec: {
title: string
content: string
kind: TemplateKind
}

View File

@@ -2,24 +2,13 @@
package v0alpha1
// +k8s:openapi-gen=true
type TemplateGroupTemplateKind string
const (
TemplateGroupTemplateKindGrafana TemplateGroupTemplateKind = "grafana"
TemplateGroupTemplateKindMimir TemplateGroupTemplateKind = "mimir"
)
// +k8s:openapi-gen=true
type TemplateGroupSpec struct {
Title string `json:"title"`
Content string `json:"content"`
Kind TemplateGroupTemplateKind `json:"kind"`
Title string `json:"title"`
Content string `json:"content"`
}
// NewTemplateGroupSpec creates a new TemplateGroupSpec object.
func NewTemplateGroupSpec() *TemplateGroupSpec {
return &TemplateGroupSpec{
Kind: TemplateGroupTemplateKindGrafana,
}
return &TemplateGroupSpec{}
}

View File

@@ -26,7 +26,7 @@ var (
rawSchemaRoutingTreev0alpha1 = []byte(`{"Matcher":{"additionalProperties":false,"properties":{"label":{"type":"string"},"type":{"enum":["=","!=","=~","!~"],"type":"string"},"value":{"type":"string"}},"required":["type","label","value"],"type":"object"},"Route":{"additionalProperties":false,"properties":{"active_time_intervals":{"items":{"type":"string"},"type":"array"},"continue":{"type":"boolean"},"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"matchers":{"items":{"$ref":"#/components/schemas/Matcher"},"type":"array"},"mute_time_intervals":{"items":{"type":"string"},"type":"array"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["continue"],"type":"object"},"RouteDefaults":{"additionalProperties":false,"properties":{"group_by":{"items":{"type":"string"},"type":"array"},"group_interval":{"type":"string"},"group_wait":{"type":"string"},"receiver":{"type":"string"},"repeat_interval":{"type":"string"}},"required":["receiver"],"type":"object"},"RoutingTree":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"defaults":{"$ref":"#/components/schemas/RouteDefaults"},"routes":{"items":{"$ref":"#/components/schemas/Route"},"type":"array"}},"required":["defaults","routes"],"type":"object"}}`)
versionSchemaRoutingTreev0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaRoutingTreev0alpha1, &versionSchemaRoutingTreev0alpha1)
rawSchemaTemplateGroupv0alpha1 = []byte(`{"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"TemplateKind":{"enum":["grafana","mimir"],"type":"string"},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"kind":{"$ref":"#/components/schemas/TemplateKind","default":"grafana"},"title":{"type":"string"}},"required":["title","content","kind"],"type":"object"}}`)
rawSchemaTemplateGroupv0alpha1 = []byte(`{"TemplateGroup":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"content":{"type":"string"},"title":{"type":"string"}},"required":["title","content"],"type":"object"}}`)
versionSchemaTemplateGroupv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaTemplateGroupv0alpha1, &versionSchemaTemplateGroupv0alpha1)
rawSchemaTimeIntervalv0alpha1 = []byte(`{"Interval":{"additionalProperties":false,"properties":{"days_of_month":{"items":{"type":"string"},"type":"array"},"location":{"type":"string"},"months":{"items":{"type":"string"},"type":"array"},"times":{"items":{"$ref":"#/components/schemas/TimeRange"},"type":"array"},"weekdays":{"items":{"type":"string"},"type":"array"},"years":{"items":{"type":"string"},"type":"array"}},"type":"object"},"TimeInterval":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"TimeRange":{"additionalProperties":false,"properties":{"end_time":{"type":"string"},"start_time":{"type":"string"}},"required":["start_time","end_time"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"name":{"type":"string"},"time_intervals":{"items":{"$ref":"#/components/schemas/Interval"},"type":"array"}},"required":["name","time_intervals"],"type":"object"}}`)

View File

@@ -71,11 +71,12 @@
"id": 1,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": true,
"spotlight": false,
"gradient": false
},
"orientation": "auto",
@@ -149,11 +150,12 @@
"id": 4,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": true,
"rounded": true,
"spotlight": false,
"gradient": false
},
"orientation": "auto",
@@ -227,11 +229,12 @@
"id": 3,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": false,
"gradient": false
},
"orientation": "auto",
@@ -268,6 +271,85 @@
"title": "Center and bar glow",
"type": "radialbar"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 12,
"y": 1
},
"id": 5,
"maxDataPoints": 20,
"options": {
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": false
},
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"segmentCount": 1,
"segmentSpacing": 0.3,
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
{
"alias": "1",
"datasource": {
"type": "grafana-testdata-datasource"
},
"max": 100,
"min": 1,
"noise": 22,
"refId": "A",
"scenarioId": "random_walk",
"spread": 22,
"startValue": 1
}
],
"title": "Spotlight",
"type": "radialbar"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
@@ -309,9 +391,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -387,9 +470,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": false,
"spotlight": true,
"gradient": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -465,9 +549,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": false,
"spotlight": true,
"gradient": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -556,9 +641,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -634,9 +720,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -712,9 +799,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -790,9 +878,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -885,9 +974,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -963,9 +1053,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1041,9 +1132,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1119,9 +1211,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1197,9 +1290,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1292,9 +1386,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1374,9 +1469,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1456,9 +1552,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1544,13 +1641,13 @@
"options": {
"barWidth": 12,
"barWidthFactor": 0.4,
"barShape": "rounded",
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": true
},
"endpointMarker": "glow",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1565,7 +1662,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1632,9 +1730,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1649,7 +1748,8 @@
"shape": "gauge",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": true
"sparkline": true,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1730,9 +1830,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1747,7 +1848,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1815,6 +1917,9 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"sparkline": false,
"spotlight": true,
"gradient": true
},
"glow": "both",
@@ -1829,10 +1934,10 @@
"segmentCount": 12,
"segmentSpacing": 0.3,
"shape": "circle",
"barShape": "rounded",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1899,9 +2004,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1916,7 +2022,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1983,9 +2090,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"rounded": true,
"spotlight": true,
"gradient": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -2000,7 +2108,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [

View File

@@ -955,6 +955,8 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
},
"orientation": "auto",

View File

@@ -1,142 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "bom-in-links-test",
"namespace": "org-1",
"labels": {
"test": "bom-stripping"
}
},
"spec": {
"title": "BOM Stripping Test Dashboard",
"description": "Testing that BOM characters are stripped from URLs during conversion",
"schemaVersion": 42,
"tags": ["test", "bom"],
"editable": true,
"links": [
{
"title": "Dashboard link with BOM",
"type": "link",
"url": "http://example.com?var=${datasource}&other=value",
"targetBlank": true,
"icon": "external link"
}
],
"panels": [
{
"id": 1,
"type": "table",
"title": "Panel with BOM in field config override links",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "green"},
{"color": "red", "value": 80}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "server"
},
"properties": [
{
"id": "links",
"value": [
{
"title": "Override link with BOM",
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}&var-server=${__value.raw}"
}
]
}
]
}
]
},
"links": [
{
"title": "Panel data link with BOM",
"url": "http://example.com/${__data.fields.cluster}&var=value",
"targetBlank": true
}
],
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "test-ds"
}
}
]
},
{
"id": 2,
"type": "timeseries",
"title": "Panel with BOM in options dataLinks",
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"options": {
"legend": {
"showLegend": true,
"displayMode": "list",
"placement": "bottom"
},
"dataLinks": [
{
"title": "Options data link with BOM",
"url": "http://example.com?series=${__series.name}&time=${__value.time}",
"targetBlank": true
}
]
},
"fieldConfig": {
"defaults": {
"links": [
{
"title": "Field config default link with BOM",
"url": "http://example.com?field=${__field.name}&value=${__value.raw}",
"targetBlank": false
}
]
},
"overrides": []
},
"targets": [
{
"refId": "A",
"datasource": {
"type": "prometheus",
"uid": "test-ds"
}
}
]
}
],
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m"]
}
}
}

View File

@@ -1,166 +0,0 @@
{
"kind": "DashboardWithAccessInfo",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "groupby-test"
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"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"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.4.0-pre",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"editorMode": "code",
"expr": "sum(counters_requests)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "works with group by var",
"type": "timeseries"
}
],
"preload": false,
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
],
"value": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
]
},
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"name": "Group by",
"type": "groupby"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "groupby test",
"weekStart": ""
}
}

View File

@@ -120,7 +120,7 @@
"value": [
{
"title": "filter",
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
}
]
}

View File

@@ -124,7 +124,7 @@
"value": [
{
"title": "filter",
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
"url": "http://localhost:3000/d/-Y-tnEDWk/templating-nested-template-variables?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
}
]
}

View File

@@ -77,12 +77,13 @@
"id": 1,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -155,12 +156,13 @@
"id": 4,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -233,12 +235,13 @@
"id": 3,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -274,6 +277,85 @@
"title": "Center and bar glow",
"type": "radialbar"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 12,
"y": 1
},
"id": 5,
"maxDataPoints": 20,
"options": {
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"segmentCount": 1,
"segmentSpacing": 0.3,
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
{
"alias": "1",
"datasource": {
"type": "grafana-testdata-datasource"
},
"max": 100,
"min": 1,
"noise": 22,
"refId": "A",
"scenarioId": "random_walk",
"spread": 22,
"startValue": 1
}
],
"title": "Spotlight",
"type": "radialbar"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
@@ -311,12 +393,13 @@
"id": 8,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -389,12 +472,13 @@
"id": 22,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -467,12 +551,13 @@
"id": 23,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -558,12 +643,13 @@
"id": 18,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.1,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -636,12 +722,13 @@
"id": 19,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.32,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -714,12 +801,13 @@
"id": 20,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.57,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -792,12 +880,13 @@
"id": 21,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.8,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -887,12 +976,13 @@
"id": 25,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -965,12 +1055,13 @@
"id": 26,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1043,12 +1134,13 @@
"id": 29,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1121,12 +1213,13 @@
"id": 30,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1199,12 +1292,13 @@
"id": 28,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1294,12 +1388,13 @@
"id": 32,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1376,12 +1471,13 @@
"id": 34,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1458,12 +1554,13 @@
"id": 33,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1548,15 +1645,15 @@
"id": 9,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"endpointMarker": "glow",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1571,7 +1668,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1633,13 +1731,14 @@
"id": 11,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1655,7 +1754,8 @@
"shape": "gauge",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": true
"sparkline": true,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1731,13 +1831,14 @@
"id": 13,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1753,7 +1854,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1816,13 +1918,15 @@
"id": 14,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"sparkline": false,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1838,7 +1942,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1900,13 +2005,14 @@
"id": 15,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.84,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1922,7 +2028,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1984,13 +2091,14 @@
"id": 16,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.66,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -2006,7 +2114,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [

View File

@@ -73,12 +73,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -164,13 +165,14 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -186,7 +188,8 @@
"shape": "gauge",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": true
"sparkline": true,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -259,13 +262,14 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -281,7 +285,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -355,13 +360,15 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"sparkline": false,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -377,7 +384,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -451,13 +459,14 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.84,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -473,7 +482,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -546,13 +556,14 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.66,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -568,7 +579,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -641,12 +653,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.1,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -732,12 +745,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.32,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -823,12 +837,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.57,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -914,12 +929,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.8,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -1005,12 +1021,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -1096,12 +1113,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -1183,12 +1201,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1274,12 +1293,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1365,12 +1385,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1456,12 +1477,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1551,12 +1573,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1638,12 +1661,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1729,12 +1753,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1824,12 +1849,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1919,12 +1945,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -2018,12 +2045,105 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"segmentCount": 1,
"segmentSpacing": 0.3,
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
},
"fieldConfig": {
"defaults": {
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-5": {
"kind": "Panel",
"spec": {
"id": 5,
"title": "Spotlight",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "grafana-testdata-datasource",
"spec": {
"alias": "1",
"max": 100,
"min": 1,
"noise": 22,
"scenarioId": "random_walk",
"spread": 22,
"startValue": 1
}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {
"maxDataPoints": 20
}
}
},
"vizConfig": {
"kind": "radialbar",
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -2109,12 +2229,13 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -2200,15 +2321,15 @@
"spec": {
"pluginVersion": "13.0.0-pre",
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"endpointMarker": "glow",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -2223,7 +2344,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -2307,6 +2429,19 @@
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 12,
"y": 0,
"width": 4,
"height": 6,
"element": {
"kind": "ElementReference",
"name": "panel-5"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {

View File

@@ -77,12 +77,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -171,13 +172,14 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -193,7 +195,8 @@
"shape": "gauge",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": true
"sparkline": true,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -269,13 +272,14 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -291,7 +295,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -368,13 +373,15 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"sparkline": false,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -390,7 +397,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -467,13 +475,14 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.84,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -489,7 +498,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -565,13 +575,14 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.66,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -587,7 +598,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -663,12 +675,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.1,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -757,12 +770,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.32,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -851,12 +865,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.57,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -945,12 +960,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.8,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -1039,12 +1055,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -1133,12 +1150,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -1223,12 +1241,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1317,12 +1336,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1411,12 +1431,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1505,12 +1526,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1603,12 +1625,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1693,12 +1716,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1787,12 +1811,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1885,12 +1910,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1983,12 +2009,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -2085,12 +2112,108 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"segmentCount": 1,
"segmentSpacing": 0.3,
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
},
"fieldConfig": {
"defaults": {
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-5": {
"kind": "Panel",
"spec": {
"id": 5,
"title": "Spotlight",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "grafana-testdata-datasource",
"version": "v0",
"spec": {
"alias": "1",
"max": 100,
"min": 1,
"noise": 22,
"scenarioId": "random_walk",
"spread": 22,
"startValue": 1
}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {
"maxDataPoints": 20
}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "radialbar",
"version": "13.0.0-pre",
"spec": {
"options": {
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -2179,12 +2302,13 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -2273,15 +2397,15 @@
"version": "13.0.0-pre",
"spec": {
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"endpointMarker": "glow",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -2296,7 +2420,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"fieldConfig": {
"defaults": {
@@ -2380,6 +2505,19 @@
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 12,
"y": 0,
"width": 4,
"height": 6,
"element": {
"kind": "ElementReference",
"name": "panel-5"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {

View File

@@ -961,7 +961,9 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {

View File

@@ -864,7 +864,9 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {

View File

@@ -901,7 +901,9 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {

View File

@@ -1,161 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "bom-in-links-test",
"namespace": "org-1",
"labels": {
"test": "bom-stripping"
}
},
"spec": {
"description": "Testing that BOM characters are stripped from URLs during conversion",
"editable": true,
"links": [
{
"icon": "external link",
"targetBlank": true,
"title": "Dashboard link with BOM",
"type": "link",
"url": "http://example.com?var=${datasource}\u0026other=value"
}
],
"panels": [
{
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "server"
},
"properties": [
{
"id": "links",
"value": [
{
"title": "Override link with BOM",
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
}
]
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"links": [
{
"targetBlank": true,
"title": "Panel data link with BOM",
"url": "http://example.com/${__data.fields.cluster}\u0026var=value"
}
],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "test-ds"
},
"refId": "A"
}
],
"title": "Panel with BOM in field config override links",
"type": "table"
},
{
"fieldConfig": {
"defaults": {
"links": [
{
"targetBlank": false,
"title": "Field config default link with BOM",
"url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}"
}
]
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"dataLinks": [
{
"targetBlank": true,
"title": "Options data link with BOM",
"url": "http://example.com?series=${__series.name}\u0026time=${__value.time}"
}
],
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "test-ds"
},
"refId": "A"
}
],
"title": "Panel with BOM in options dataLinks",
"type": "timeseries"
}
],
"schemaVersion": 42,
"tags": [
"test",
"bom"
],
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m"
]
},
"title": "BOM Stripping Test Dashboard"
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -1,242 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v2alpha1",
"metadata": {
"name": "bom-in-links-test",
"namespace": "org-1",
"labels": {
"test": "bom-stripping"
}
},
"spec": {
"annotations": [],
"cursorSync": "Off",
"description": "Testing that BOM characters are stripped from URLs during conversion",
"editable": true,
"elements": {
"panel-1": {
"kind": "Panel",
"spec": {
"id": 1,
"title": "Panel with BOM in field config override links",
"description": "",
"links": [
{
"title": "Panel data link with BOM",
"url": "http://example.com/${__data.fields.cluster}\u0026var=value",
"targetBlank": true
}
],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "prometheus",
"spec": {}
},
"datasource": {
"type": "prometheus",
"uid": "test-ds"
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "table",
"spec": {
"pluginVersion": "",
"options": {},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "server"
},
"properties": [
{
"id": "links",
"value": [
{
"title": "Override link with BOM",
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
}
]
}
]
}
]
}
}
}
}
},
"panel-2": {
"kind": "Panel",
"spec": {
"id": 2,
"title": "Panel with BOM in options dataLinks",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "prometheus",
"spec": {}
},
"datasource": {
"type": "prometheus",
"uid": "test-ds"
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "timeseries",
"spec": {
"pluginVersion": "",
"options": {
"dataLinks": [
{
"targetBlank": true,
"title": "Options data link with BOM",
"url": "http://example.com?series=${__series.name}\u0026time=${__value.time}"
}
],
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
}
},
"fieldConfig": {
"defaults": {
"links": [
{
"targetBlank": false,
"title": "Field config default link with BOM",
"url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}"
}
]
},
"overrides": []
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 0,
"width": 12,
"height": 8,
"element": {
"kind": "ElementReference",
"name": "panel-1"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 12,
"y": 0,
"width": 12,
"height": 8,
"element": {
"kind": "ElementReference",
"name": "panel-2"
}
}
}
]
}
},
"links": [
{
"title": "Dashboard link with BOM",
"type": "link",
"icon": "external link",
"tooltip": "",
"url": "http://example.com?var=${datasource}\u0026other=value",
"tags": [],
"asDropdown": false,
"targetBlank": true,
"includeVars": false,
"keepTime": false
}
],
"liveNow": false,
"preload": false,
"tags": [
"test",
"bom"
],
"timeSettings": {
"timezone": "browser",
"from": "now-6h",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "BOM Stripping Test Dashboard",
"variables": []
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -1,246 +0,0 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v2beta1",
"metadata": {
"name": "bom-in-links-test",
"namespace": "org-1",
"labels": {
"test": "bom-stripping"
}
},
"spec": {
"annotations": [],
"cursorSync": "Off",
"description": "Testing that BOM characters are stripped from URLs during conversion",
"editable": true,
"elements": {
"panel-1": {
"kind": "Panel",
"spec": {
"id": 1,
"title": "Panel with BOM in field config override links",
"description": "",
"links": [
{
"title": "Panel data link with BOM",
"url": "http://example.com/${__data.fields.cluster}\u0026var=value",
"targetBlank": true
}
],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "prometheus",
"version": "v0",
"datasource": {
"name": "test-ds"
},
"spec": {}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "table",
"version": "",
"spec": {
"options": {},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "server"
},
"properties": [
{
"id": "links",
"value": [
{
"title": "Override link with BOM",
"url": "http://localhost:3000/d/test?var-datacenter=${__data.fields[datacenter]}\u0026var-server=${__value.raw}"
}
]
}
]
}
]
}
}
}
}
},
"panel-2": {
"kind": "Panel",
"spec": {
"id": 2,
"title": "Panel with BOM in options dataLinks",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "prometheus",
"version": "v0",
"datasource": {
"name": "test-ds"
},
"spec": {}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "timeseries",
"version": "",
"spec": {
"options": {
"dataLinks": [
{
"targetBlank": true,
"title": "Options data link with BOM",
"url": "http://example.com?series=${__series.name}\u0026time=${__value.time}"
}
],
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
}
},
"fieldConfig": {
"defaults": {
"links": [
{
"targetBlank": false,
"title": "Field config default link with BOM",
"url": "http://example.com?field=${__field.name}\u0026value=${__value.raw}"
}
]
},
"overrides": []
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 0,
"width": 12,
"height": 8,
"element": {
"kind": "ElementReference",
"name": "panel-1"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 12,
"y": 0,
"width": 12,
"height": 8,
"element": {
"kind": "ElementReference",
"name": "panel-2"
}
}
}
]
}
},
"links": [
{
"title": "Dashboard link with BOM",
"type": "link",
"icon": "external link",
"tooltip": "",
"url": "http://example.com?var=${datasource}\u0026other=value",
"tags": [],
"asDropdown": false,
"targetBlank": true,
"includeVars": false,
"keepTime": false
}
],
"liveNow": false,
"preload": false,
"tags": [
"test",
"bom"
],
"timeSettings": {
"timezone": "browser",
"from": "now-6h",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "BOM Stripping Test Dashboard",
"variables": []
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -1,172 +0,0 @@
{
"kind": "DashboardWithAccessInfo",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "groupby-test"
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"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"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.4.0-pre",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"editorMode": "code",
"expr": "sum(counters_requests)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "works with group by var",
"type": "timeseries"
}
],
"preload": false,
"schemaVersion": 42,
"tags": [],
"templating": {
"list": [
{
"current": {
"text": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
],
"value": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
]
},
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"name": "Group by",
"type": "groupby"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "groupby test",
"weekStart": ""
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -1,229 +0,0 @@
{
"kind": "DashboardWithAccessInfo",
"apiVersion": "dashboard.grafana.app/v2alpha1",
"metadata": {
"name": "groupby-test"
},
"spec": {
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"query": {
"kind": "grafana",
"spec": {}
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"builtIn": true,
"legacyOptions": {
"type": "dashboard"
}
}
}
],
"cursorSync": "Off",
"editable": true,
"elements": {
"panel-2": {
"kind": "Panel",
"spec": {
"id": 2,
"title": "works with group by var",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "prometheus",
"spec": {
"editorMode": "code",
"expr": "sum(counters_requests)",
"legendFormat": "__auto",
"range": true
}
},
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "timeseries",
"spec": {
"pluginVersion": "12.4.0-pre",
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"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"
}
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 12,
"y": 0,
"width": 12,
"height": 8,
"element": {
"kind": "ElementReference",
"name": "panel-2"
}
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [],
"timeSettings": {
"timezone": "browser",
"from": "now-6h",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "groupby test",
"variables": [
{
"kind": "GroupByVariable",
"spec": {
"name": "Group by",
"datasource": {
"type": "prometheus",
"uid": "test-uid"
},
"current": {
"text": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
],
"value": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
]
},
"options": [],
"multi": true,
"hide": "dontHide",
"skipUrlSync": false
}
}
]
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -1,232 +0,0 @@
{
"kind": "DashboardWithAccessInfo",
"apiVersion": "dashboard.grafana.app/v2beta1",
"metadata": {
"name": "groupby-test"
},
"spec": {
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "grafana",
"version": "v0",
"datasource": {
"name": "-- Grafana --"
},
"spec": {}
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"builtIn": true,
"legacyOptions": {
"type": "dashboard"
}
}
}
],
"cursorSync": "Off",
"editable": true,
"elements": {
"panel-2": {
"kind": "Panel",
"spec": {
"id": 2,
"title": "works with group by var",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "prometheus",
"version": "v0",
"datasource": {
"name": "test-uid"
},
"spec": {
"editorMode": "code",
"expr": "sum(counters_requests)",
"legendFormat": "__auto",
"range": true
}
},
"refId": "A",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "timeseries",
"version": "12.4.0-pre",
"spec": {
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"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"
}
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 12,
"y": 0,
"width": 12,
"height": 8,
"element": {
"kind": "ElementReference",
"name": "panel-2"
}
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [],
"timeSettings": {
"timezone": "browser",
"from": "now-6h",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "groupby test",
"variables": [
{
"kind": "GroupByVariable",
"group": "prometheus",
"datasource": {
"name": "test-uid"
},
"spec": {
"name": "Group by",
"current": {
"text": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
],
"value": [
"a_legacy_label",
"app",
"exported_instance",
"exported_job"
]
},
"options": [],
"multi": true,
"hide": "dontHide",
"skipUrlSync": false
}
}
]
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -229,36 +229,6 @@ func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool
return defaultValue
}
// stripBOM removes Byte Order Mark (BOM) characters from a string.
// BOMs (U+FEFF) can be introduced through copy/paste from certain editors
// and cause CUE validation errors ("illegal byte order mark").
func stripBOM(s string) string {
return strings.ReplaceAll(s, "\ufeff", "")
}
// stripBOMFromInterface recursively strips BOM characters from all strings
// in an interface{} value (map, slice, or string).
func stripBOMFromInterface(v interface{}) interface{} {
switch val := v.(type) {
case string:
return stripBOM(val)
case map[string]interface{}:
result := make(map[string]interface{}, len(val))
for k, v := range val {
result[k] = stripBOMFromInterface(v)
}
return result
case []interface{}:
result := make([]interface{}, len(val))
for i, item := range val {
result[i] = stripBOMFromInterface(item)
}
return result
default:
return v
}
}
func getUnionField[T ~string](m map[string]interface{}, key string) *T {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok && str != "" {
@@ -423,8 +393,7 @@ func transformLinks(dashboard map[string]interface{}) []dashv2alpha1.DashboardDa
// Optional field - only set if present
if url, exists := linkMap["url"]; exists {
if urlStr, ok := url.(string); ok {
cleanUrl := stripBOM(urlStr)
dashLink.Url = &cleanUrl
dashLink.Url = &urlStr
}
}
@@ -1765,9 +1734,7 @@ func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, co
Hide: commonProps.Hide,
SkipUrlSync: commonProps.SkipUrlSync,
Current: buildVariableCurrent(varMap["current"]),
// We set it to true by default because GroupByVariable
// constructor defaults to multi: true
Multi: getBoolField(varMap, "multi", true),
Multi: getBoolField(varMap, "multi", false),
},
}
@@ -2270,7 +2237,7 @@ func transformDataLinks(panelMap map[string]interface{}) []dashv2alpha1.Dashboar
if linkMap, ok := link.(map[string]interface{}); ok {
dataLink := dashv2alpha1.DashboardDataLink{
Title: schemaversion.GetStringValue(linkMap, "title"),
Url: stripBOM(schemaversion.GetStringValue(linkMap, "url")),
Url: schemaversion.GetStringValue(linkMap, "url"),
}
if _, exists := linkMap["targetBlank"]; exists {
targetBlank := getBoolField(linkMap, "targetBlank", false)
@@ -2362,12 +2329,6 @@ func buildVizConfig(panelMap map[string]interface{}) dashv2alpha1.DashboardVizCo
}
}
// Strip BOMs from options (may contain dataLinks with URLs that have BOMs)
cleanedOptions := stripBOMFromInterface(options)
if cleanedMap, ok := cleanedOptions.(map[string]interface{}); ok {
options = cleanedMap
}
// Build field config by mapping each field individually
fieldConfigSource := extractFieldConfigSource(fieldConfig)
@@ -2511,14 +2472,9 @@ func extractFieldConfigDefaults(defaults map[string]interface{}) dashv2alpha1.Da
hasDefaults = true
}
// Extract array field - strip BOMs from link URLs
// Extract array field
if linksArray, ok := extractArrayField(defaults, "links"); ok {
cleanedLinks := stripBOMFromInterface(linksArray)
if cleanedArray, ok := cleanedLinks.([]interface{}); ok {
fieldConfigDefaults.Links = cleanedArray
} else {
fieldConfigDefaults.Links = linksArray
}
fieldConfigDefaults.Links = linksArray
hasDefaults = true
}
@@ -2804,11 +2760,9 @@ func extractFieldConfigOverrides(fieldConfig map[string]interface{}) []dashv2alp
fieldOverride.Properties = make([]dashv2alpha1.DashboardDynamicConfigValue, 0, len(propertiesArray))
for _, property := range propertiesArray {
if propertyMap, ok := property.(map[string]interface{}); ok {
// Strip BOMs from property values (may contain links with URLs)
cleanedValue := stripBOMFromInterface(propertyMap["value"])
fieldOverride.Properties = append(fieldOverride.Properties, dashv2alpha1.DashboardDynamicConfigValue{
Id: schemaversion.GetStringValue(propertyMap, "id"),
Value: cleanedValue,
Value: propertyMap["value"],
})
}
}

View File

@@ -75,9 +75,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -153,9 +154,10 @@
"effects": {
"barGlow": false,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -231,9 +233,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -302,6 +305,85 @@
"x": 12,
"y": 1
},
"id": 5,
"maxDataPoints": 20,
"options": {
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"segmentCount": 1,
"segmentSpacing": 0.3,
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
{
"alias": "1",
"datasource": {
"type": "grafana-testdata-datasource"
},
"max": 100,
"min": 1,
"noise": 22,
"refId": "A",
"scenarioId": "random_walk",
"spread": 22,
"startValue": 1
}
],
"title": "Spotlight",
"type": "radialbar"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 16,
"y": 1
},
"id": 8,
"maxDataPoints": 20,
"options": {
@@ -309,9 +391,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -377,8 +460,8 @@
"gridPos": {
"h": 6,
"w": 4,
"x": 16,
"y": 1
"x": 0,
"y": 7
},
"id": 22,
"maxDataPoints": 20,
@@ -387,9 +470,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -455,8 +539,8 @@
"gridPos": {
"h": 6,
"w": 4,
"x": 20,
"y": 1
"x": 4,
"y": 7
},
"id": 23,
"maxDataPoints": 20,
@@ -465,9 +549,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -508,7 +593,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 7
"y": 13
},
"id": 17,
"panels": [],
@@ -545,9 +630,9 @@
},
"gridPos": {
"h": 6,
"w": 4,
"w": 5,
"x": 0,
"y": 8
"y": 14
},
"id": 18,
"maxDataPoints": 20,
@@ -556,9 +641,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -623,9 +709,9 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 4,
"y": 8
"w": 5,
"x": 5,
"y": 14
},
"id": 19,
"maxDataPoints": 20,
@@ -634,9 +720,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -701,9 +788,9 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 8,
"y": 8
"w": 5,
"x": 10,
"y": 14
},
"id": 20,
"maxDataPoints": 20,
@@ -712,9 +799,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -779,9 +867,9 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 12,
"y": 8
"w": 5,
"x": 15,
"y": 14
},
"id": 21,
"maxDataPoints": 20,
@@ -790,9 +878,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -833,7 +922,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 14
"y": 20
},
"id": 24,
"panels": [],
@@ -874,9 +963,9 @@
},
"gridPos": {
"h": 6,
"w": 4,
"w": 6,
"x": 0,
"y": 15
"y": 21
},
"id": 25,
"maxDataPoints": 20,
@@ -885,9 +974,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -952,9 +1042,9 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 4,
"y": 15
"w": 6,
"x": 6,
"y": 21
},
"id": 26,
"maxDataPoints": 20,
@@ -963,9 +1053,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1030,9 +1121,9 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 8,
"y": 15
"w": 5,
"x": 12,
"y": 21
},
"id": 29,
"maxDataPoints": 20,
@@ -1041,9 +1132,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1107,10 +1199,10 @@
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 12,
"y": 15
"h": 7,
"w": 6,
"x": 0,
"y": 27
},
"id": 30,
"maxDataPoints": 20,
@@ -1119,9 +1211,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1185,10 +1278,10 @@
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 16,
"y": 15
"h": 7,
"w": 6,
"x": 6,
"y": 27
},
"id": 28,
"maxDataPoints": 20,
@@ -1197,9 +1290,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1236,7 +1330,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 21
"y": 34
},
"id": 31,
"panels": [],
@@ -1283,7 +1377,7 @@
"h": 10,
"w": 7,
"x": 0,
"y": 22
"y": 35
},
"id": 32,
"maxDataPoints": 20,
@@ -1292,9 +1386,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1365,7 +1460,7 @@
"h": 10,
"w": 7,
"x": 7,
"y": 22
"y": 35
},
"id": 34,
"maxDataPoints": 20,
@@ -1374,9 +1469,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1447,7 +1543,7 @@
"h": 10,
"w": 6,
"x": 14,
"y": 22
"y": 35
},
"id": 33,
"maxDataPoints": 20,
@@ -1456,9 +1552,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -1495,7 +1592,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 32
"y": 45
},
"id": 6,
"panels": [],
@@ -1536,20 +1633,20 @@
"h": 6,
"w": 24,
"x": 0,
"y": 33
"y": 46
},
"id": 9,
"maxDataPoints": 20,
"options": {
"barWidth": 12,
"barWidthFactor": 0.4,
"barShape": "rounded",
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"endpointMarker": "glow",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1564,7 +1661,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1619,7 +1717,7 @@
"h": 6,
"w": 24,
"x": 0,
"y": 39
"y": 52
},
"id": 11,
"maxDataPoints": 20,
@@ -1629,9 +1727,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1646,7 +1745,8 @@
"shape": "gauge",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": true
"sparkline": true,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1673,7 +1773,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 45
"y": 58
},
"id": 12,
"panels": [],
@@ -1715,7 +1815,7 @@
"h": 7,
"w": 4,
"x": 0,
"y": 46
"y": 59
},
"id": 13,
"maxDataPoints": 20,
@@ -1725,9 +1825,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1742,7 +1843,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1798,7 +1900,7 @@
"h": 7,
"w": 5,
"x": 4,
"y": 46
"y": 59
},
"id": 14,
"maxDataPoints": 20,
@@ -1808,9 +1910,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1825,7 +1928,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1880,7 +1984,7 @@
"h": 7,
"w": 5,
"x": 9,
"y": 46
"y": 59
},
"id": 15,
"maxDataPoints": 20,
@@ -1890,9 +1994,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1907,7 +2012,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1962,7 +2068,7 @@
"h": 7,
"w": 6,
"x": 14,
"y": 46
"y": 59
},
"id": 16,
"maxDataPoints": 20,
@@ -1972,9 +2078,10 @@
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"barShape": "rounded",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1989,7 +2096,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -2016,7 +2124,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 53
"y": 66
},
"id": 35,
"panels": [],
@@ -2047,10 +2155,10 @@
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"h": 8,
"w": 6,
"x": 0,
"y": 54
"y": 67
},
"id": 36,
"options": {
@@ -2058,9 +2166,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -2114,10 +2223,10 @@
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 54
"h": 8,
"w": 6,
"x": 6,
"y": 67
},
"id": 37,
"options": {
@@ -2125,9 +2234,10 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"barShape": "flat",
"orientation": "auto",
"reduceOptions": {
"calcs": [
@@ -2169,4 +2279,4 @@
"title": "Panel tests - Gauge (new)",
"uid": "panel-tests-gauge-new",
"weekStart": ""
}
}

View File

@@ -955,7 +955,9 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1160,4 +1162,4 @@
"title": "Panel tests - Old gauge to new",
"uid": "panel-tests-old-gauge-to-new",
"weekStart": ""
}
}

View File

@@ -223,7 +223,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 // indirect
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7 // indirect
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect

View File

@@ -827,8 +827,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7 h1:ZzG/gCclEit9w0QUfQt9GURcOycAIGcsQAhY1u0AEX0=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=

View File

@@ -24,7 +24,6 @@ require (
require (
cel.dev/expr v0.25.1 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
@@ -36,21 +35,16 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
github.com/aws/smithy-go v1.23.1 // indirect
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -90,7 +84,7 @@ require (
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 // indirect
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7 // indirect
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
@@ -100,7 +94,6 @@ require (
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
github.com/grafana/grafana/pkg/apimachinery v0.0.0 // indirect
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grafana/sqlds/v4 v4.2.7 // indirect
@@ -149,15 +142,11 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/nikunjy/rules v1.5.0 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/open-feature/go-sdk v1.16.0 // indirect
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // indirect
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
@@ -176,7 +165,6 @@ require (
github.com/spf13/pflag v1.0.10 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect
github.com/tjhop/slog-gokit v0.1.5 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
@@ -191,7 +179,6 @@ require (
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
@@ -199,8 +186,6 @@ require (
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect

View File

@@ -4,13 +4,9 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -42,18 +38,12 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
@@ -70,8 +60,6 @@ github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wX
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
@@ -81,8 +69,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 h1:92HSmB9bwM/o0ZvrCpcvTP2EsPXSkKtAniIr2W/dcIM=
github.com/diegoholiveira/jsonlogic/v3 v3.7.4/go.mod h1:OYRb6FSTVmMM+MNQ7ElmMsczyNSepw+OU4Z8emDSi4w=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
@@ -213,8 +199,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196 h1:A9UJtyBBUE7PkRsAITKU05iz+HpHO9SaVjfdo2Df3UQ=
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7 h1:ZzG/gCclEit9w0QUfQt9GURcOycAIGcsQAhY1u0AEX0=
github.com/grafana/alerting v0.0.0-20251212143239-491433b332b7/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
@@ -233,8 +219,6 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:2HRzUK/xQEYc+8d5If/XSusMcaYq9IptnBSHACiQcOQ=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 h1:aXfUhVN/Ewfpbko2CCtL65cIiGgwStOo4lWH2b6gw2U=
@@ -382,8 +366,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nikunjy/rules v1.5.0 h1:KJDSLOsFhwt7kcXUyZqwkgrQg5YoUwj+TVu6ItCQShw=
github.com/nikunjy/rules v1.5.0/go.mod h1:TlZtZdBChrkqi8Lr2AXocme8Z7EsbxtFdDoKeI6neBQ=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
@@ -398,12 +380,6 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg=
github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo=
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 h1:megzzlQGjsRVWDX8oJnLaa5eEcsAHekiL4Uvl3jSAcY=
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6/go.mod h1:K1gDKvt76CGFLSUMHUydd5ba2V5Cv69gQZsdbnXhAm8=
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 h1:WinefYxeVx5rV0uQmuWbxQf8iACu/JiRubo5w0saToc=
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6/go.mod h1:Dwcaoma6lZVqYwyfVlY7eB6RXbG+Ju3b9cnpTlUN+Hc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@@ -489,10 +465,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM=
github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8=
github.com/thomaspoignant/go-feature-flag v1.42.0 h1:C7embmOTzaLyRki+OoU2RvtVjJE9IrvgBA2C1mRN1lc=
github.com/thomaspoignant/go-feature-flag v1.42.0/go.mod h1:y0QiWH7chHWhGATb/+XqwAwErORmPSH2MUsQlCmmWlM=
github.com/tjhop/slog-gokit v0.1.5 h1:ayloIUi5EK2QYB8eY4DOPO95/mRtMW42lUkp3quJohc=
github.com/tjhop/slog-gokit v0.1.5/go.mod h1:yA48zAHvV+Sg4z4VRyeFyFUNNXd3JY5Zg84u3USICq0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
@@ -541,8 +513,6 @@ go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0/go.mod h1:B9Oka5QVD0bn
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
@@ -562,12 +532,8 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=

View File

@@ -5,25 +5,7 @@ metaV0Alpha1: {
scope: "Namespaced"
schema: {
spec: {
pluginJson: #JSONData
class: "core" | "external"
module?: {
path: string
hash?: string
loadingStrategy?: "fetch" | "script"
}
baseURL?: string
signature?: {
status: "internal" | "valid" | "invalid" | "modified" | "unsigned"
type?: "grafana" | "commercial" | "community" | "private" | "private-glob"
org?: string
}
angular?: {
detected: bool
}
translations?: [string]: string
// +listType=atomic
children?: [...string]
pluginJSON: #JSONData,
}
}
}

View File

@@ -9,6 +9,7 @@ pluginV0Alpha1: {
id: string
version: string
url?: string
class: "core" | "external"
}
}
}

View File

@@ -208,21 +208,13 @@ func NewMetaExtensions() *MetaExtensions {
// +k8s:openapi-gen=true
type MetaSpec struct {
PluginJson MetaJSONData `json:"pluginJson"`
Class MetaSpecClass `json:"class"`
Module *MetaV0alpha1SpecModule `json:"module,omitempty"`
BaseURL *string `json:"baseURL,omitempty"`
Signature *MetaV0alpha1SpecSignature `json:"signature,omitempty"`
Angular *MetaV0alpha1SpecAngular `json:"angular,omitempty"`
Translations map[string]string `json:"translations,omitempty"`
// +listType=atomic
Children []string `json:"children,omitempty"`
PluginJSON MetaJSONData `json:"pluginJSON"`
}
// NewMetaSpec creates a new MetaSpec object.
func NewMetaSpec() *MetaSpec {
return &MetaSpec{
PluginJson: *NewMetaJSONData(),
PluginJSON: *NewMetaJSONData(),
}
}
@@ -420,40 +412,6 @@ func NewMetaV0alpha1ExtensionsExtensionPoints() *MetaV0alpha1ExtensionsExtension
return &MetaV0alpha1ExtensionsExtensionPoints{}
}
// +k8s:openapi-gen=true
type MetaV0alpha1SpecModule struct {
Path string `json:"path"`
Hash *string `json:"hash,omitempty"`
LoadingStrategy *MetaV0alpha1SpecModuleLoadingStrategy `json:"loadingStrategy,omitempty"`
}
// NewMetaV0alpha1SpecModule creates a new MetaV0alpha1SpecModule object.
func NewMetaV0alpha1SpecModule() *MetaV0alpha1SpecModule {
return &MetaV0alpha1SpecModule{}
}
// +k8s:openapi-gen=true
type MetaV0alpha1SpecSignature struct {
Status MetaV0alpha1SpecSignatureStatus `json:"status"`
Type *MetaV0alpha1SpecSignatureType `json:"type,omitempty"`
Org *string `json:"org,omitempty"`
}
// NewMetaV0alpha1SpecSignature creates a new MetaV0alpha1SpecSignature object.
func NewMetaV0alpha1SpecSignature() *MetaV0alpha1SpecSignature {
return &MetaV0alpha1SpecSignature{}
}
// +k8s:openapi-gen=true
type MetaV0alpha1SpecAngular struct {
Detected bool `json:"detected"`
}
// NewMetaV0alpha1SpecAngular creates a new MetaV0alpha1SpecAngular object.
func NewMetaV0alpha1SpecAngular() *MetaV0alpha1SpecAngular {
return &MetaV0alpha1SpecAngular{}
}
// +k8s:openapi-gen=true
type MetaJSONDataType string
@@ -506,14 +464,6 @@ const (
MetaIncludeRoleViewer MetaIncludeRole = "Viewer"
)
// +k8s:openapi-gen=true
type MetaSpecClass string
const (
MetaSpecClassCore MetaSpecClass = "core"
MetaSpecClassExternal MetaSpecClass = "external"
)
// +k8s:openapi-gen=true
type MetaV0alpha1DependenciesPluginsType string
@@ -522,33 +472,3 @@ const (
MetaV0alpha1DependenciesPluginsTypeDatasource MetaV0alpha1DependenciesPluginsType = "datasource"
MetaV0alpha1DependenciesPluginsTypePanel MetaV0alpha1DependenciesPluginsType = "panel"
)
// +k8s:openapi-gen=true
type MetaV0alpha1SpecModuleLoadingStrategy string
const (
MetaV0alpha1SpecModuleLoadingStrategyFetch MetaV0alpha1SpecModuleLoadingStrategy = "fetch"
MetaV0alpha1SpecModuleLoadingStrategyScript MetaV0alpha1SpecModuleLoadingStrategy = "script"
)
// +k8s:openapi-gen=true
type MetaV0alpha1SpecSignatureStatus string
const (
MetaV0alpha1SpecSignatureStatusInternal MetaV0alpha1SpecSignatureStatus = "internal"
MetaV0alpha1SpecSignatureStatusValid MetaV0alpha1SpecSignatureStatus = "valid"
MetaV0alpha1SpecSignatureStatusInvalid MetaV0alpha1SpecSignatureStatus = "invalid"
MetaV0alpha1SpecSignatureStatusModified MetaV0alpha1SpecSignatureStatus = "modified"
MetaV0alpha1SpecSignatureStatusUnsigned MetaV0alpha1SpecSignatureStatus = "unsigned"
)
// +k8s:openapi-gen=true
type MetaV0alpha1SpecSignatureType string
const (
MetaV0alpha1SpecSignatureTypeGrafana MetaV0alpha1SpecSignatureType = "grafana"
MetaV0alpha1SpecSignatureTypeCommercial MetaV0alpha1SpecSignatureType = "commercial"
MetaV0alpha1SpecSignatureTypeCommunity MetaV0alpha1SpecSignatureType = "community"
MetaV0alpha1SpecSignatureTypePrivate MetaV0alpha1SpecSignatureType = "private"
MetaV0alpha1SpecSignatureTypePrivateGlob MetaV0alpha1SpecSignatureType = "private-glob"
)

View File

@@ -4,12 +4,21 @@ package v0alpha1
// +k8s:openapi-gen=true
type PluginSpec struct {
Id string `json:"id"`
Version string `json:"version"`
Url *string `json:"url,omitempty"`
Id string `json:"id"`
Version string `json:"version"`
Url *string `json:"url,omitempty"`
Class PluginSpecClass `json:"class"`
}
// NewPluginSpec creates a new PluginSpec object.
func NewPluginSpec() *PluginSpec {
return &PluginSpec{}
}
// +k8s:openapi-gen=true
type PluginSpecClass string
const (
PluginSpecClassCore PluginSpecClass = "core"
PluginSpecClassExternal PluginSpecClass = "external"
)

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,16 @@ const (
PluginInstallSourceAnnotation = "plugins.grafana.app/install-source"
)
// Class represents the plugin class type in an unversioned internal format.
// This intentionally duplicates the versioned API type (PluginInstallSpecClass) to decouple
// internal code from API version changes, making it easier to support multiple API versions.
type Class = string
const (
ClassCore Class = "core"
ClassExternal Class = "external"
)
type Source = string
const (
@@ -26,6 +36,7 @@ type PluginInstall struct {
ID string
Version string
URL string
Class Class
Source Source
}
@@ -46,6 +57,7 @@ func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alph
Id: p.ID,
Version: p.Version,
Url: url,
Class: pluginsv0alpha1.PluginSpecClass(p.Class),
},
}
}
@@ -58,6 +70,9 @@ func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.Plugin) bool {
if existing.Spec.Version != update.Spec.Version {
return true
}
if existing.Spec.Class != update.Spec.Class {
return true // this should never really happen
}
if !equalStringPointers(existing.Spec.Url, update.Spec.Url) {
return true
}

View File

@@ -26,12 +26,14 @@ func TestPluginInstall_ShouldUpdate(t *testing.T) {
Spec: pluginsv0alpha1.PluginSpec{
Id: "plugin-1",
Version: "1.0.0",
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
},
}
baseInstall := PluginInstall{
ID: "plugin-1",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
}
@@ -52,6 +54,13 @@ func TestPluginInstall_ShouldUpdate(t *testing.T) {
},
expectUpdate: true,
},
{
name: "class differs",
modifyInstall: func(pi *PluginInstall) {
pi.Class = ClassCore
},
expectUpdate: true,
},
{
name: "url differs",
modifyInstall: func(pi *PluginInstall) {
@@ -100,6 +109,7 @@ func TestInstallRegistrar_Register(t *testing.T) {
install: &PluginInstall{
ID: "plugin-1",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
existingErr: errorsK8s.NewNotFound(pluginGroupResource(), "plugin-1"),
@@ -110,6 +120,7 @@ func TestInstallRegistrar_Register(t *testing.T) {
install: &PluginInstall{
ID: "plugin-1",
Version: "2.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
existing: &pluginsv0alpha1.Plugin{
@@ -124,6 +135,7 @@ func TestInstallRegistrar_Register(t *testing.T) {
Spec: pluginsv0alpha1.PluginSpec{
Id: "plugin-1",
Version: "1.0.0",
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
},
},
expectedUpdates: 1,
@@ -133,6 +145,7 @@ func TestInstallRegistrar_Register(t *testing.T) {
install: &PluginInstall{
ID: "plugin-1",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
existing: &pluginsv0alpha1.Plugin{
@@ -147,6 +160,7 @@ func TestInstallRegistrar_Register(t *testing.T) {
Spec: pluginsv0alpha1.PluginSpec{
Id: "plugin-1",
Version: "1.0.0",
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
},
},
},
@@ -155,6 +169,7 @@ func TestInstallRegistrar_Register(t *testing.T) {
install: &PluginInstall{
ID: "plugin-err",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
existingErr: errorsK8s.NewInternalError(errors.New("boom")),
@@ -395,6 +410,7 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) {
install: PluginInstall{
ID: "plugin-1",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
namespace: "org-1",
@@ -408,6 +424,7 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) {
ID: "plugin-1",
Version: "1.0.0",
URL: "https://example.com/plugin.zip",
Class: ClassExternal,
Source: SourcePluginStore,
},
namespace: "org-1",
@@ -416,11 +433,25 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) {
require.Equal(t, "https://example.com/plugin.zip", *p.Spec.Url)
},
},
{
name: "core class is mapped correctly",
install: PluginInstall{
ID: "plugin-core",
Version: "2.0.0",
Class: ClassCore,
Source: SourcePluginStore,
},
namespace: "org-2",
validate: func(t *testing.T, p *pluginsv0alpha1.Plugin) {
require.Equal(t, pluginsv0alpha1.PluginSpecClass(ClassCore), p.Spec.Class)
},
},
{
name: "source annotation is set correctly",
install: PluginInstall{
ID: "plugin-1",
Version: "1.0.0",
Class: ClassExternal,
Source: SourceUnknown,
},
namespace: "org-1",
@@ -433,6 +464,7 @@ func TestPluginInstall_ToPluginInstallV0Alpha1(t *testing.T) {
install: PluginInstall{
ID: "my-plugin",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
namespace: "my-namespace",
@@ -524,6 +556,7 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) {
ID: "plugin-1",
Version: "1.0.0",
URL: newURL,
Class: ClassExternal,
Source: SourcePluginStore,
},
existingURL: nil,
@@ -535,6 +568,7 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) {
ID: "plugin-1",
Version: "1.0.0",
URL: "",
Class: ClassExternal,
Source: SourcePluginStore,
},
existingURL: &existingURL,
@@ -546,6 +580,7 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) {
ID: "plugin-1",
Version: "1.0.0",
URL: "",
Class: ClassExternal,
Source: SourcePluginStore,
},
existingURL: nil,
@@ -557,6 +592,7 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) {
ID: "plugin-1",
Version: "1.0.0",
URL: existingURL,
Class: ClassExternal,
Source: SourcePluginStore,
},
existingURL: &existingURL,
@@ -578,6 +614,7 @@ func TestPluginInstall_ShouldUpdate_URLTransitions(t *testing.T) {
Id: "plugin-1",
Version: "1.0.0",
Url: tt.existingURL,
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
},
}
@@ -633,6 +670,7 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) {
install: &PluginInstall{
ID: "plugin-1",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
setupClient: func(fc *fakePluginInstallClient) {
@@ -650,6 +688,7 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) {
install: &PluginInstall{
ID: "plugin-1",
Version: "2.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
},
setupClient: func(fc *fakePluginInstallClient) {
@@ -666,6 +705,7 @@ func TestInstallRegistrar_Register_ErrorCases(t *testing.T) {
Spec: pluginsv0alpha1.PluginSpec{
Id: "plugin-1",
Version: "1.0.0",
Class: pluginsv0alpha1.PluginSpecClass(ClassExternal),
},
}, nil
}
@@ -836,6 +876,7 @@ func TestInstallRegistrar_GetClientError(t *testing.T) {
install := &PluginInstall{
ID: "plugin-1",
Version: "1.0.0",
Class: ClassExternal,
Source: SourcePluginStore,
}

View File

@@ -10,6 +10,8 @@ import (
"time"
"github.com/grafana/grafana-app-sdk/logging"
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
)
const (
@@ -85,9 +87,45 @@ func (p *CatalogProvider) GetMeta(ctx context.Context, pluginID, version string)
return nil, fmt.Errorf("failed to decode response: %w", err)
}
metaSpec := grafanaComPluginVersionMetaToMetaSpec(gcomMeta)
return &Result{
Meta: metaSpec,
Meta: gcomMeta.JSON,
TTL: p.ttl,
}, nil
}
// grafanaComPluginVersionMeta represents the response from grafana.com API
// GET /api/plugins/{pluginId}/versions/{version}
type grafanaComPluginVersionMeta struct {
PluginID string `json:"pluginSlug"`
Version string `json:"version"`
URL string `json:"url"`
Commit string `json:"commit"`
Description string `json:"description"`
Keywords []string `json:"keywords"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
JSON pluginsv0alpha1.MetaJSONData `json:"json"`
Readme string `json:"readme"`
Downloads int `json:"downloads"`
Verified bool `json:"verified"`
Status string `json:"status"`
StatusContext string `json:"statusContext"`
DownloadSlug string `json:"downloadSlug"`
SignatureType string `json:"signatureType"`
SignedByOrg string `json:"signedByOrg"`
SignedByOrgName string `json:"signedByOrgName"`
Packages struct {
Any struct {
Md5 string `json:"md5"`
Sha256 string `json:"sha256"`
PackageName string `json:"packageName"`
DownloadURL string `json:"downloadUrl"`
} `json:"any"`
} `json:"packages"`
Links []struct {
Rel string `json:"rel"`
Href string `json:"href"`
} `json:"links"`
AngularDetected bool `json:"angularDetected"`
Scopes []string `json:"scopes"`
}

View File

@@ -49,7 +49,7 @@ func TestCatalogProvider_GetMeta(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, expectedMeta, result.Meta.PluginJson)
assert.Equal(t, expectedMeta, result.Meta)
assert.Equal(t, defaultCatalogTTL, result.TTL)
})

View File

@@ -1,744 +0,0 @@
package meta
import (
"encoding/json"
"time"
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
// jsonDataToMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.MetaJSONData.
// nolint:gocyclo
func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSONData {
meta := pluginsv0alpha1.MetaJSONData{
Id: jsonData.ID,
Name: jsonData.Name,
}
// Map plugin type
switch jsonData.Type {
case plugins.TypeApp:
meta.Type = pluginsv0alpha1.MetaJSONDataTypeApp
case plugins.TypeDataSource:
meta.Type = pluginsv0alpha1.MetaJSONDataTypeDatasource
case plugins.TypePanel:
meta.Type = pluginsv0alpha1.MetaJSONDataTypePanel
case plugins.TypeRenderer:
meta.Type = pluginsv0alpha1.MetaJSONDataTypeRenderer
}
// Map Info
meta.Info = pluginsv0alpha1.MetaInfo{
Keywords: jsonData.Info.Keywords,
Logos: pluginsv0alpha1.MetaV0alpha1InfoLogos{
Small: jsonData.Info.Logos.Small,
Large: jsonData.Info.Logos.Large,
},
Updated: jsonData.Info.Updated,
Version: jsonData.Info.Version,
}
if jsonData.Info.Description != "" {
meta.Info.Description = &jsonData.Info.Description
}
if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" {
author := &pluginsv0alpha1.MetaV0alpha1InfoAuthor{}
if jsonData.Info.Author.Name != "" {
author.Name = &jsonData.Info.Author.Name
}
if jsonData.Info.Author.URL != "" {
author.Url = &jsonData.Info.Author.URL
}
meta.Info.Author = author
}
if len(jsonData.Info.Links) > 0 {
meta.Info.Links = make([]pluginsv0alpha1.MetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links))
for _, link := range jsonData.Info.Links {
v0Link := pluginsv0alpha1.MetaV0alpha1InfoLinks{}
if link.Name != "" {
v0Link.Name = &link.Name
}
if link.URL != "" {
v0Link.Url = &link.URL
}
meta.Info.Links = append(meta.Info.Links, v0Link)
}
}
if len(jsonData.Info.Screenshots) > 0 {
meta.Info.Screenshots = make([]pluginsv0alpha1.MetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots))
for _, screenshot := range jsonData.Info.Screenshots {
v0Screenshot := pluginsv0alpha1.MetaV0alpha1InfoScreenshots{}
if screenshot.Name != "" {
v0Screenshot.Name = &screenshot.Name
}
if screenshot.Path != "" {
v0Screenshot.Path = &screenshot.Path
}
meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot)
}
}
// Map Dependencies
meta.Dependencies = pluginsv0alpha1.MetaDependencies{
GrafanaDependency: jsonData.Dependencies.GrafanaDependency,
}
if jsonData.Dependencies.GrafanaVersion != "" {
meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion
}
if len(jsonData.Dependencies.Plugins) > 0 {
meta.Dependencies.Plugins = make([]pluginsv0alpha1.MetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins))
for _, dep := range jsonData.Dependencies.Plugins {
var depType pluginsv0alpha1.MetaV0alpha1DependenciesPluginsType
switch dep.Type {
case "app":
depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeApp
case "datasource":
depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeDatasource
case "panel":
depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypePanel
}
meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.MetaV0alpha1DependenciesPlugins{
Id: dep.ID,
Type: depType,
Name: dep.Name,
})
}
}
if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 {
meta.Dependencies.Extensions = &pluginsv0alpha1.MetaV0alpha1DependenciesExtensions{
ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents,
}
}
// Map optional boolean fields
if jsonData.Alerting {
meta.Alerting = &jsonData.Alerting
}
if jsonData.Annotations {
meta.Annotations = &jsonData.Annotations
}
if jsonData.AutoEnabled {
meta.AutoEnabled = &jsonData.AutoEnabled
}
if jsonData.Backend {
meta.Backend = &jsonData.Backend
}
if jsonData.BuiltIn {
meta.BuiltIn = &jsonData.BuiltIn
}
if jsonData.HideFromList {
meta.HideFromList = &jsonData.HideFromList
}
if jsonData.Logs {
meta.Logs = &jsonData.Logs
}
if jsonData.Metrics {
meta.Metrics = &jsonData.Metrics
}
if jsonData.MultiValueFilterOperators {
meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators
}
if jsonData.Preload {
meta.Preload = &jsonData.Preload
}
if jsonData.SkipDataQuery {
meta.SkipDataQuery = &jsonData.SkipDataQuery
}
if jsonData.Streaming {
meta.Streaming = &jsonData.Streaming
}
if jsonData.Tracing {
meta.Tracing = &jsonData.Tracing
}
// Map category
if jsonData.Category != "" {
var category pluginsv0alpha1.MetaJSONDataCategory
switch jsonData.Category {
case "tsdb":
category = pluginsv0alpha1.MetaJSONDataCategoryTsdb
case "logging":
category = pluginsv0alpha1.MetaJSONDataCategoryLogging
case "cloud":
category = pluginsv0alpha1.MetaJSONDataCategoryCloud
case "tracing":
category = pluginsv0alpha1.MetaJSONDataCategoryTracing
case "profiling":
category = pluginsv0alpha1.MetaJSONDataCategoryProfiling
case "sql":
category = pluginsv0alpha1.MetaJSONDataCategorySql
case "enterprise":
category = pluginsv0alpha1.MetaJSONDataCategoryEnterprise
case "iot":
category = pluginsv0alpha1.MetaJSONDataCategoryIot
case "other":
category = pluginsv0alpha1.MetaJSONDataCategoryOther
default:
category = pluginsv0alpha1.MetaJSONDataCategoryOther
}
meta.Category = &category
}
// Map state
if jsonData.State != "" {
var state pluginsv0alpha1.MetaJSONDataState
switch jsonData.State {
case plugins.ReleaseStateAlpha:
state = pluginsv0alpha1.MetaJSONDataStateAlpha
case plugins.ReleaseStateBeta:
state = pluginsv0alpha1.MetaJSONDataStateBeta
default:
}
if state != "" {
meta.State = &state
}
}
// Map executable
if jsonData.Executable != "" {
meta.Executable = &jsonData.Executable
}
// Map QueryOptions
if len(jsonData.QueryOptions) > 0 {
queryOptions := &pluginsv0alpha1.MetaQueryOptions{}
if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok {
queryOptions.MaxDataPoints = &val
}
if val, ok := jsonData.QueryOptions["minInterval"]; ok {
queryOptions.MinInterval = &val
}
if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok {
queryOptions.CacheTimeout = &val
}
meta.QueryOptions = queryOptions
}
// Map Includes
if len(jsonData.Includes) > 0 {
meta.Includes = make([]pluginsv0alpha1.MetaInclude, 0, len(jsonData.Includes))
for _, include := range jsonData.Includes {
v0Include := pluginsv0alpha1.MetaInclude{}
if include.UID != "" {
v0Include.Uid = &include.UID
}
if include.Type != "" {
var includeType pluginsv0alpha1.MetaIncludeType
switch include.Type {
case "dashboard":
includeType = pluginsv0alpha1.MetaIncludeTypeDashboard
case "page":
includeType = pluginsv0alpha1.MetaIncludeTypePage
case "panel":
includeType = pluginsv0alpha1.MetaIncludeTypePanel
case "datasource":
includeType = pluginsv0alpha1.MetaIncludeTypeDatasource
}
v0Include.Type = &includeType
}
if include.Name != "" {
v0Include.Name = &include.Name
}
if include.Component != "" {
v0Include.Component = &include.Component
}
if include.Role != "" {
var role pluginsv0alpha1.MetaIncludeRole
switch include.Role {
case "Admin":
role = pluginsv0alpha1.MetaIncludeRoleAdmin
case "Editor":
role = pluginsv0alpha1.MetaIncludeRoleEditor
case "Viewer":
role = pluginsv0alpha1.MetaIncludeRoleViewer
}
v0Include.Role = &role
}
if include.Action != "" {
v0Include.Action = &include.Action
}
if include.Path != "" {
v0Include.Path = &include.Path
}
if include.AddToNav {
v0Include.AddToNav = &include.AddToNav
}
if include.DefaultNav {
v0Include.DefaultNav = &include.DefaultNav
}
if include.Icon != "" {
v0Include.Icon = &include.Icon
}
meta.Includes = append(meta.Includes, v0Include)
}
}
// Map Routes
if len(jsonData.Routes) > 0 {
meta.Routes = make([]pluginsv0alpha1.MetaRoute, 0, len(jsonData.Routes))
for _, route := range jsonData.Routes {
v0Route := pluginsv0alpha1.MetaRoute{}
if route.Path != "" {
v0Route.Path = &route.Path
}
if route.Method != "" {
v0Route.Method = &route.Method
}
if route.URL != "" {
v0Route.Url = &route.URL
}
if route.ReqRole != "" {
reqRole := string(route.ReqRole)
v0Route.ReqRole = &reqRole
}
if route.ReqAction != "" {
v0Route.ReqAction = &route.ReqAction
}
if len(route.Headers) > 0 {
headers := make([]string, 0, len(route.Headers))
for _, header := range route.Headers {
headers = append(headers, header.Name+": "+header.Content)
}
v0Route.Headers = headers
}
if len(route.URLParams) > 0 {
v0Route.UrlParams = make([]pluginsv0alpha1.MetaV0alpha1RouteUrlParams, 0, len(route.URLParams))
for _, param := range route.URLParams {
v0Param := pluginsv0alpha1.MetaV0alpha1RouteUrlParams{}
if param.Name != "" {
v0Param.Name = &param.Name
}
if param.Content != "" {
v0Param.Content = &param.Content
}
v0Route.UrlParams = append(v0Route.UrlParams, v0Param)
}
}
if route.TokenAuth != nil {
v0Route.TokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteTokenAuth{}
if route.TokenAuth.Url != "" {
v0Route.TokenAuth.Url = &route.TokenAuth.Url
}
if len(route.TokenAuth.Scopes) > 0 {
v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes
}
if len(route.TokenAuth.Params) > 0 {
v0Route.TokenAuth.Params = make(map[string]interface{})
for k, v := range route.TokenAuth.Params {
v0Route.TokenAuth.Params[k] = v
}
}
}
if route.JwtTokenAuth != nil {
v0Route.JwtTokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteJwtTokenAuth{}
if route.JwtTokenAuth.Url != "" {
v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url
}
if len(route.JwtTokenAuth.Scopes) > 0 {
v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes
}
if len(route.JwtTokenAuth.Params) > 0 {
v0Route.JwtTokenAuth.Params = make(map[string]interface{})
for k, v := range route.JwtTokenAuth.Params {
v0Route.JwtTokenAuth.Params[k] = v
}
}
}
if len(route.Body) > 0 {
var bodyMap map[string]interface{}
if err := json.Unmarshal(route.Body, &bodyMap); err == nil {
v0Route.Body = bodyMap
}
}
meta.Routes = append(meta.Routes, v0Route)
}
}
// Map Extensions
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
extensions := &pluginsv0alpha1.MetaExtensions{}
if len(jsonData.Extensions.AddedLinks) > 0 {
extensions.AddedLinks = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks))
for _, link := range jsonData.Extensions.AddedLinks {
v0Link := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks{
Targets: link.Targets,
Title: link.Title,
}
if link.Description != "" {
v0Link.Description = &link.Description
}
extensions.AddedLinks = append(extensions.AddedLinks, v0Link)
}
}
if len(jsonData.Extensions.AddedComponents) > 0 {
extensions.AddedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents))
for _, comp := range jsonData.Extensions.AddedComponents {
v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents{
Targets: comp.Targets,
Title: comp.Title,
}
if comp.Description != "" {
v0Comp.Description = &comp.Description
}
extensions.AddedComponents = append(extensions.AddedComponents, v0Comp)
}
}
if len(jsonData.Extensions.ExposedComponents) > 0 {
extensions.ExposedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
for _, comp := range jsonData.Extensions.ExposedComponents {
v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents{
Id: comp.Id,
}
if comp.Title != "" {
v0Comp.Title = &comp.Title
}
if comp.Description != "" {
v0Comp.Description = &comp.Description
}
extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp)
}
}
if len(jsonData.Extensions.ExtensionPoints) > 0 {
extensions.ExtensionPoints = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints))
for _, point := range jsonData.Extensions.ExtensionPoints {
v0Point := pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints{
Id: point.Id,
}
if point.Title != "" {
v0Point.Title = &point.Title
}
if point.Description != "" {
v0Point.Description = &point.Description
}
extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point)
}
}
meta.Extensions = extensions
}
// Map Roles
if len(jsonData.Roles) > 0 {
meta.Roles = make([]pluginsv0alpha1.MetaRole, 0, len(jsonData.Roles))
for _, role := range jsonData.Roles {
v0Role := pluginsv0alpha1.MetaRole{
Grants: role.Grants,
}
if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 {
v0RoleRole := &pluginsv0alpha1.MetaV0alpha1RoleRole{}
if role.Role.Name != "" {
v0RoleRole.Name = &role.Role.Name
}
if role.Role.Description != "" {
v0RoleRole.Description = &role.Role.Description
}
if len(role.Role.Permissions) > 0 {
v0RoleRole.Permissions = make([]pluginsv0alpha1.MetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions))
for _, perm := range role.Role.Permissions {
v0Perm := pluginsv0alpha1.MetaV0alpha1RoleRolePermissions{}
if perm.Action != "" {
v0Perm.Action = &perm.Action
}
if perm.Scope != "" {
v0Perm.Scope = &perm.Scope
}
v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm)
}
}
v0Role.Role = v0RoleRole
}
meta.Roles = append(meta.Roles, v0Role)
}
}
// Map IAM
if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 {
iam := &pluginsv0alpha1.MetaIAM{
Permissions: make([]pluginsv0alpha1.MetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)),
}
for _, perm := range jsonData.IAM.Permissions {
v0Perm := pluginsv0alpha1.MetaV0alpha1IAMPermissions{}
if perm.Action != "" {
v0Perm.Action = &perm.Action
}
if perm.Scope != "" {
v0Perm.Scope = &perm.Scope
}
iam.Permissions = append(iam.Permissions, v0Perm)
}
meta.Iam = iam
}
return meta
}
// pluginStorePluginToMeta converts a pluginstore.Plugin to a pluginsv0alpha1.MetaSpec.
// This is similar to pluginToPluginMetaSpec but works with the plugin store DTO.
// loadingStrategy and moduleHash are optional calculated values that can be provided.
func pluginStorePluginToMeta(plugin pluginstore.Plugin, loadingStrategy plugins.LoadingStrategy, moduleHash string) pluginsv0alpha1.MetaSpec {
metaSpec := pluginsv0alpha1.MetaSpec{
PluginJson: jsonDataToMetaJSONData(plugin.JSONData),
}
// Set Class - default to External if not specified
var c pluginsv0alpha1.MetaSpecClass
if plugin.Class == plugins.ClassCore {
c = pluginsv0alpha1.MetaSpecClassCore
} else {
c = pluginsv0alpha1.MetaSpecClassExternal
}
metaSpec.Class = c
if plugin.Module != "" {
module := &pluginsv0alpha1.MetaV0alpha1SpecModule{
Path: plugin.Module,
}
if moduleHash != "" {
module.Hash = &moduleHash
}
if loadingStrategy != "" {
var ls pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategy
switch loadingStrategy {
case plugins.LoadingStrategyFetch:
ls = pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyFetch
case plugins.LoadingStrategyScript:
ls = pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyScript
}
module.LoadingStrategy = &ls
}
metaSpec.Module = module
}
if plugin.BaseURL != "" {
metaSpec.BaseURL = &plugin.BaseURL
}
if plugin.Signature != "" {
signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{
Status: convertSignatureStatus(plugin.Signature),
}
if plugin.SignatureType != "" {
sigType := convertSignatureType(plugin.SignatureType)
signature.Type = &sigType
}
if plugin.SignatureOrg != "" {
signature.Org = &plugin.SignatureOrg
}
metaSpec.Signature = signature
}
if len(plugin.Children) > 0 {
metaSpec.Children = plugin.Children
}
metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{
Detected: plugin.Angular.Detected,
}
if len(plugin.Translations) > 0 {
metaSpec.Translations = plugin.Translations
}
return metaSpec
}
// convertSignatureStatus converts plugins.SignatureStatus to pluginsv0alpha1.MetaV0alpha1SpecSignatureStatus.
func convertSignatureStatus(status plugins.SignatureStatus) pluginsv0alpha1.MetaV0alpha1SpecSignatureStatus {
switch status {
case plugins.SignatureStatusInternal:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusInternal
case plugins.SignatureStatusValid:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusValid
case plugins.SignatureStatusInvalid:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusInvalid
case plugins.SignatureStatusModified:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusModified
case plugins.SignatureStatusUnsigned:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusUnsigned
default:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusUnsigned
}
}
// convertSignatureType converts plugins.SignatureType to pluginsv0alpha1.MetaV0alpha1SpecSignatureType.
func convertSignatureType(sigType plugins.SignatureType) pluginsv0alpha1.MetaV0alpha1SpecSignatureType {
switch sigType {
case plugins.SignatureTypeGrafana:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana
case plugins.SignatureTypeCommercial:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommercial
case plugins.SignatureTypeCommunity:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommunity
case plugins.SignatureTypePrivate:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivate
case plugins.SignatureTypePrivateGlob:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivateGlob
default:
return pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana
}
}
// pluginToMetaSpec converts a fully loaded *plugins.Plugin to a pluginsv0alpha1.MetaSpec.
func pluginToMetaSpec(plugin *plugins.Plugin) pluginsv0alpha1.MetaSpec {
metaSpec := pluginsv0alpha1.MetaSpec{
PluginJson: jsonDataToMetaJSONData(plugin.JSONData),
}
// Set Class - default to External if not specified
var c pluginsv0alpha1.MetaSpecClass
if plugin.Class == plugins.ClassCore {
c = pluginsv0alpha1.MetaSpecClassCore
} else {
c = pluginsv0alpha1.MetaSpecClassExternal
}
metaSpec.Class = c
// Set module information
if plugin.Module != "" {
module := &pluginsv0alpha1.MetaV0alpha1SpecModule{
Path: plugin.Module,
}
loadingStrategy := pluginsv0alpha1.MetaV0alpha1SpecModuleLoadingStrategyScript
module.LoadingStrategy = &loadingStrategy
metaSpec.Module = module
}
// Set BaseURL
if plugin.BaseURL != "" {
metaSpec.BaseURL = &plugin.BaseURL
}
// Set signature information
signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{
Status: convertSignatureStatus(plugin.Signature),
}
if plugin.SignatureType != "" {
sigType := convertSignatureType(plugin.SignatureType)
signature.Type = &sigType
}
if plugin.SignatureOrg != "" {
signature.Org = &plugin.SignatureOrg
}
metaSpec.Signature = signature
if len(plugin.Children) > 0 {
children := make([]string, 0, len(plugin.Children))
for _, child := range plugin.Children {
children = append(children, child.ID)
}
metaSpec.Children = children
}
metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{
Detected: plugin.Angular.Detected,
}
if len(plugin.Translations) > 0 {
metaSpec.Translations = plugin.Translations
}
return metaSpec
}
// grafanaComPluginVersionMeta represents the response from grafana.com API
// GET /api/plugins/{pluginId}/versions/{version}
type grafanaComPluginVersionMeta struct {
PluginID string `json:"pluginSlug"`
Version string `json:"version"`
URL string `json:"url"`
Commit string `json:"commit"`
Description string `json:"description"`
Keywords []string `json:"keywords"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
JSON pluginsv0alpha1.MetaJSONData `json:"json"`
Readme string `json:"readme"`
Downloads int `json:"downloads"`
Verified bool `json:"verified"`
Status string `json:"status"`
StatusContext string `json:"statusContext"`
DownloadSlug string `json:"downloadSlug"`
SignatureType string `json:"signatureType"`
SignedByOrg string `json:"signedByOrg"`
SignedByOrgName string `json:"signedByOrgName"`
Packages struct {
Any struct {
Md5 string `json:"md5"`
Sha256 string `json:"sha256"`
PackageName string `json:"packageName"`
DownloadURL string `json:"downloadUrl"`
} `json:"any"`
} `json:"packages"`
Links []struct {
Rel string `json:"rel"`
Href string `json:"href"`
} `json:"links"`
AngularDetected bool `json:"angularDetected"`
Scopes []string `json:"scopes"`
}
// grafanaComPluginVersionMetaToMetaSpec converts a grafanaComPluginVersionMeta to a pluginsv0alpha1.MetaSpec.
func grafanaComPluginVersionMetaToMetaSpec(gcomMeta grafanaComPluginVersionMeta) pluginsv0alpha1.MetaSpec {
metaSpec := pluginsv0alpha1.MetaSpec{
PluginJson: gcomMeta.JSON,
Class: pluginsv0alpha1.MetaSpecClassExternal,
}
if gcomMeta.SignatureType != "" {
signature := &pluginsv0alpha1.MetaV0alpha1SpecSignature{
Status: pluginsv0alpha1.MetaV0alpha1SpecSignatureStatusValid,
}
switch gcomMeta.SignatureType {
case "grafana":
sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeGrafana
signature.Type = &sigType
case "commercial":
sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommercial
signature.Type = &sigType
case "community":
sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypeCommunity
signature.Type = &sigType
case "private":
sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivate
signature.Type = &sigType
case "private-glob":
sigType := pluginsv0alpha1.MetaV0alpha1SpecSignatureTypePrivateGlob
signature.Type = &sigType
}
if gcomMeta.SignedByOrg != "" {
signature.Org = &gcomMeta.SignedByOrg
}
metaSpec.Signature = signature
}
// Set angular info
metaSpec.Angular = &pluginsv0alpha1.MetaV0alpha1SpecAngular{
Detected: gcomMeta.AngularDetected,
}
return metaSpec
}

View File

@@ -2,6 +2,7 @@ package meta
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
@@ -12,15 +13,7 @@ import (
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
pluginsLoader "github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/termination"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
)
const (
@@ -30,10 +23,9 @@ const (
// CoreProvider retrieves plugin metadata for core plugins.
type CoreProvider struct {
mu sync.RWMutex
loadedPlugins map[string]pluginsv0alpha1.MetaSpec
loadedPlugins map[string]pluginsv0alpha1.MetaJSONData
initialized bool
ttl time.Duration
loader pluginsLoader.Service
}
// NewCoreProvider creates a new CoreProvider for core plugins.
@@ -43,13 +35,9 @@ func NewCoreProvider() *CoreProvider {
// NewCoreProviderWithTTL creates a new CoreProvider with a custom TTL.
func NewCoreProviderWithTTL(ttl time.Duration) *CoreProvider {
cfg := &config.PluginManagementCfg{
Features: config.Features{},
}
return &CoreProvider{
loadedPlugins: make(map[string]pluginsv0alpha1.MetaSpec),
loadedPlugins: make(map[string]pluginsv0alpha1.MetaJSONData),
ttl: ttl,
loader: createLoader(cfg),
}
}
@@ -88,9 +76,9 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result
p.initialized = true
}
if spec, found := p.loadedPlugins[pluginID]; found {
if meta, found := p.loadedPlugins[pluginID]; found {
return &Result{
Meta: spec,
Meta: meta,
TTL: p.ttl,
}, nil
}
@@ -98,8 +86,8 @@ func (p *CoreProvider) GetMeta(ctx context.Context, pluginID, _ string) (*Result
return nil, ErrMetaNotFound
}
// loadPlugins discovers and caches all core plugins by fully loading them.
// Returns an error if the static root path cannot be found or if plugin loading fails.
// loadPlugins discovers and caches all core plugins.
// Returns an error if the static root path cannot be found or if plugin discovery fails.
// This error will be handled gracefully by GetMeta, which will return ErrMetaNotFound
// to allow other providers to handle the request.
func (p *CoreProvider) loadPlugins(ctx context.Context) error {
@@ -120,51 +108,496 @@ func (p *CoreProvider) loadPlugins(ctx context.Context) error {
panelPath := filepath.Join(staticRootPath, "app", "plugins", "panel")
src := sources.NewLocalSource(plugins.ClassCore, []string{datasourcePath, panelPath})
loadedPlugins, err := p.loader.Load(ctx, src)
ps, err := src.Discover(ctx)
if err != nil {
return err
}
if len(loadedPlugins) == 0 {
logging.DefaultLogger.Warn("CoreProvider: no core plugins found during loading")
if len(ps) == 0 {
logging.DefaultLogger.Warn("CoreProvider: no core plugins found during discovery")
return nil
}
for _, plugin := range loadedPlugins {
metaSpec := pluginToMetaSpec(plugin)
p.loadedPlugins[plugin.ID] = metaSpec
for _, bundle := range ps {
meta := jsonDataToMetaJSONData(bundle.Primary.JSONData)
p.loadedPlugins[bundle.Primary.JSONData.ID] = meta
}
return nil
}
// createLoader creates a loader service configured for core plugins.
func createLoader(cfg *config.PluginManagementCfg) pluginsLoader.Service {
d := discovery.New(cfg, discovery.Opts{
FilterFuncs: []discovery.FilterFunc{
// Allow all plugin types for core plugins
},
})
b := bootstrap.New(cfg, bootstrap.Opts{
DecorateFuncs: []bootstrap.DecorateFunc{}, // no decoration required for metadata
})
v := validation.New(cfg, validation.Opts{
ValidateFuncs: []validation.ValidateFunc{
// Skip validation for core plugins - they're trusted
},
})
i := initialization.New(cfg, initialization.Opts{
InitializeFuncs: []initialization.InitializeFunc{
// Skip initialization - we only need metadata, not running plugins
},
})
t, _ := termination.New(cfg, termination.Opts{
TerminateFuncs: []termination.TerminateFunc{
// No termination needed for metadata-only loading
},
})
// jsonDataToMetaJSONData converts a plugins.JSONData to a pluginsv0alpha1.MetaJSONData.
// nolint:gocyclo
func jsonDataToMetaJSONData(jsonData plugins.JSONData) pluginsv0alpha1.MetaJSONData {
meta := pluginsv0alpha1.MetaJSONData{
Id: jsonData.ID,
Name: jsonData.Name,
}
et := pluginerrs.ProvideErrorTracker()
// Map plugin type
switch jsonData.Type {
case plugins.TypeApp:
meta.Type = pluginsv0alpha1.MetaJSONDataTypeApp
case plugins.TypeDataSource:
meta.Type = pluginsv0alpha1.MetaJSONDataTypeDatasource
case plugins.TypePanel:
meta.Type = pluginsv0alpha1.MetaJSONDataTypePanel
case plugins.TypeRenderer:
meta.Type = pluginsv0alpha1.MetaJSONDataTypeRenderer
}
return pluginsLoader.New(cfg, d, b, v, i, t, et)
// Map Info
meta.Info = pluginsv0alpha1.MetaInfo{
Keywords: jsonData.Info.Keywords,
Logos: pluginsv0alpha1.MetaV0alpha1InfoLogos{
Small: jsonData.Info.Logos.Small,
Large: jsonData.Info.Logos.Large,
},
Updated: jsonData.Info.Updated,
Version: jsonData.Info.Version,
}
if jsonData.Info.Description != "" {
meta.Info.Description = &jsonData.Info.Description
}
if jsonData.Info.Author.Name != "" || jsonData.Info.Author.URL != "" {
author := &pluginsv0alpha1.MetaV0alpha1InfoAuthor{}
if jsonData.Info.Author.Name != "" {
author.Name = &jsonData.Info.Author.Name
}
if jsonData.Info.Author.URL != "" {
author.Url = &jsonData.Info.Author.URL
}
meta.Info.Author = author
}
if len(jsonData.Info.Links) > 0 {
meta.Info.Links = make([]pluginsv0alpha1.MetaV0alpha1InfoLinks, 0, len(jsonData.Info.Links))
for _, link := range jsonData.Info.Links {
v0Link := pluginsv0alpha1.MetaV0alpha1InfoLinks{}
if link.Name != "" {
v0Link.Name = &link.Name
}
if link.URL != "" {
v0Link.Url = &link.URL
}
meta.Info.Links = append(meta.Info.Links, v0Link)
}
}
if len(jsonData.Info.Screenshots) > 0 {
meta.Info.Screenshots = make([]pluginsv0alpha1.MetaV0alpha1InfoScreenshots, 0, len(jsonData.Info.Screenshots))
for _, screenshot := range jsonData.Info.Screenshots {
v0Screenshot := pluginsv0alpha1.MetaV0alpha1InfoScreenshots{}
if screenshot.Name != "" {
v0Screenshot.Name = &screenshot.Name
}
if screenshot.Path != "" {
v0Screenshot.Path = &screenshot.Path
}
meta.Info.Screenshots = append(meta.Info.Screenshots, v0Screenshot)
}
}
// Map Dependencies
meta.Dependencies = pluginsv0alpha1.MetaDependencies{
GrafanaDependency: jsonData.Dependencies.GrafanaDependency,
}
if jsonData.Dependencies.GrafanaVersion != "" {
meta.Dependencies.GrafanaVersion = &jsonData.Dependencies.GrafanaVersion
}
if len(jsonData.Dependencies.Plugins) > 0 {
meta.Dependencies.Plugins = make([]pluginsv0alpha1.MetaV0alpha1DependenciesPlugins, 0, len(jsonData.Dependencies.Plugins))
for _, dep := range jsonData.Dependencies.Plugins {
var depType pluginsv0alpha1.MetaV0alpha1DependenciesPluginsType
switch dep.Type {
case "app":
depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeApp
case "datasource":
depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypeDatasource
case "panel":
depType = pluginsv0alpha1.MetaV0alpha1DependenciesPluginsTypePanel
}
meta.Dependencies.Plugins = append(meta.Dependencies.Plugins, pluginsv0alpha1.MetaV0alpha1DependenciesPlugins{
Id: dep.ID,
Type: depType,
Name: dep.Name,
})
}
}
if len(jsonData.Dependencies.Extensions.ExposedComponents) > 0 {
meta.Dependencies.Extensions = &pluginsv0alpha1.MetaV0alpha1DependenciesExtensions{
ExposedComponents: jsonData.Dependencies.Extensions.ExposedComponents,
}
}
// Map optional boolean fields
if jsonData.Alerting {
meta.Alerting = &jsonData.Alerting
}
if jsonData.Annotations {
meta.Annotations = &jsonData.Annotations
}
if jsonData.AutoEnabled {
meta.AutoEnabled = &jsonData.AutoEnabled
}
if jsonData.Backend {
meta.Backend = &jsonData.Backend
}
if jsonData.BuiltIn {
meta.BuiltIn = &jsonData.BuiltIn
}
if jsonData.HideFromList {
meta.HideFromList = &jsonData.HideFromList
}
if jsonData.Logs {
meta.Logs = &jsonData.Logs
}
if jsonData.Metrics {
meta.Metrics = &jsonData.Metrics
}
if jsonData.MultiValueFilterOperators {
meta.MultiValueFilterOperators = &jsonData.MultiValueFilterOperators
}
if jsonData.Preload {
meta.Preload = &jsonData.Preload
}
if jsonData.SkipDataQuery {
meta.SkipDataQuery = &jsonData.SkipDataQuery
}
if jsonData.Streaming {
meta.Streaming = &jsonData.Streaming
}
if jsonData.Tracing {
meta.Tracing = &jsonData.Tracing
}
// Map category
if jsonData.Category != "" {
var category pluginsv0alpha1.MetaJSONDataCategory
switch jsonData.Category {
case "tsdb":
category = pluginsv0alpha1.MetaJSONDataCategoryTsdb
case "logging":
category = pluginsv0alpha1.MetaJSONDataCategoryLogging
case "cloud":
category = pluginsv0alpha1.MetaJSONDataCategoryCloud
case "tracing":
category = pluginsv0alpha1.MetaJSONDataCategoryTracing
case "profiling":
category = pluginsv0alpha1.MetaJSONDataCategoryProfiling
case "sql":
category = pluginsv0alpha1.MetaJSONDataCategorySql
case "enterprise":
category = pluginsv0alpha1.MetaJSONDataCategoryEnterprise
case "iot":
category = pluginsv0alpha1.MetaJSONDataCategoryIot
case "other":
category = pluginsv0alpha1.MetaJSONDataCategoryOther
default:
category = pluginsv0alpha1.MetaJSONDataCategoryOther
}
meta.Category = &category
}
// Map state
if jsonData.State != "" {
var state pluginsv0alpha1.MetaJSONDataState
switch jsonData.State {
case plugins.ReleaseStateAlpha:
state = pluginsv0alpha1.MetaJSONDataStateAlpha
case plugins.ReleaseStateBeta:
state = pluginsv0alpha1.MetaJSONDataStateBeta
default:
}
if state != "" {
meta.State = &state
}
}
// Map executable
if jsonData.Executable != "" {
meta.Executable = &jsonData.Executable
}
// Map QueryOptions
if len(jsonData.QueryOptions) > 0 {
queryOptions := &pluginsv0alpha1.MetaQueryOptions{}
if val, ok := jsonData.QueryOptions["maxDataPoints"]; ok {
queryOptions.MaxDataPoints = &val
}
if val, ok := jsonData.QueryOptions["minInterval"]; ok {
queryOptions.MinInterval = &val
}
if val, ok := jsonData.QueryOptions["cacheTimeout"]; ok {
queryOptions.CacheTimeout = &val
}
meta.QueryOptions = queryOptions
}
// Map Includes
if len(jsonData.Includes) > 0 {
meta.Includes = make([]pluginsv0alpha1.MetaInclude, 0, len(jsonData.Includes))
for _, include := range jsonData.Includes {
v0Include := pluginsv0alpha1.MetaInclude{}
if include.UID != "" {
v0Include.Uid = &include.UID
}
if include.Type != "" {
var includeType pluginsv0alpha1.MetaIncludeType
switch include.Type {
case "dashboard":
includeType = pluginsv0alpha1.MetaIncludeTypeDashboard
case "page":
includeType = pluginsv0alpha1.MetaIncludeTypePage
case "panel":
includeType = pluginsv0alpha1.MetaIncludeTypePanel
case "datasource":
includeType = pluginsv0alpha1.MetaIncludeTypeDatasource
}
v0Include.Type = &includeType
}
if include.Name != "" {
v0Include.Name = &include.Name
}
if include.Component != "" {
v0Include.Component = &include.Component
}
if include.Role != "" {
var role pluginsv0alpha1.MetaIncludeRole
switch include.Role {
case "Admin":
role = pluginsv0alpha1.MetaIncludeRoleAdmin
case "Editor":
role = pluginsv0alpha1.MetaIncludeRoleEditor
case "Viewer":
role = pluginsv0alpha1.MetaIncludeRoleViewer
}
v0Include.Role = &role
}
if include.Action != "" {
v0Include.Action = &include.Action
}
if include.Path != "" {
v0Include.Path = &include.Path
}
if include.AddToNav {
v0Include.AddToNav = &include.AddToNav
}
if include.DefaultNav {
v0Include.DefaultNav = &include.DefaultNav
}
if include.Icon != "" {
v0Include.Icon = &include.Icon
}
meta.Includes = append(meta.Includes, v0Include)
}
}
// Map Routes
if len(jsonData.Routes) > 0 {
meta.Routes = make([]pluginsv0alpha1.MetaRoute, 0, len(jsonData.Routes))
for _, route := range jsonData.Routes {
v0Route := pluginsv0alpha1.MetaRoute{}
if route.Path != "" {
v0Route.Path = &route.Path
}
if route.Method != "" {
v0Route.Method = &route.Method
}
if route.URL != "" {
v0Route.Url = &route.URL
}
if route.ReqRole != "" {
reqRole := string(route.ReqRole)
v0Route.ReqRole = &reqRole
}
if route.ReqAction != "" {
v0Route.ReqAction = &route.ReqAction
}
if len(route.Headers) > 0 {
headers := make([]string, 0, len(route.Headers))
for _, header := range route.Headers {
headers = append(headers, header.Name+": "+header.Content)
}
v0Route.Headers = headers
}
if len(route.URLParams) > 0 {
v0Route.UrlParams = make([]pluginsv0alpha1.MetaV0alpha1RouteUrlParams, 0, len(route.URLParams))
for _, param := range route.URLParams {
v0Param := pluginsv0alpha1.MetaV0alpha1RouteUrlParams{}
if param.Name != "" {
v0Param.Name = &param.Name
}
if param.Content != "" {
v0Param.Content = &param.Content
}
v0Route.UrlParams = append(v0Route.UrlParams, v0Param)
}
}
if route.TokenAuth != nil {
v0Route.TokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteTokenAuth{}
if route.TokenAuth.Url != "" {
v0Route.TokenAuth.Url = &route.TokenAuth.Url
}
if len(route.TokenAuth.Scopes) > 0 {
v0Route.TokenAuth.Scopes = route.TokenAuth.Scopes
}
if len(route.TokenAuth.Params) > 0 {
v0Route.TokenAuth.Params = make(map[string]interface{})
for k, v := range route.TokenAuth.Params {
v0Route.TokenAuth.Params[k] = v
}
}
}
if route.JwtTokenAuth != nil {
v0Route.JwtTokenAuth = &pluginsv0alpha1.MetaV0alpha1RouteJwtTokenAuth{}
if route.JwtTokenAuth.Url != "" {
v0Route.JwtTokenAuth.Url = &route.JwtTokenAuth.Url
}
if len(route.JwtTokenAuth.Scopes) > 0 {
v0Route.JwtTokenAuth.Scopes = route.JwtTokenAuth.Scopes
}
if len(route.JwtTokenAuth.Params) > 0 {
v0Route.JwtTokenAuth.Params = make(map[string]interface{})
for k, v := range route.JwtTokenAuth.Params {
v0Route.JwtTokenAuth.Params[k] = v
}
}
}
if len(route.Body) > 0 {
var bodyMap map[string]interface{}
if err := json.Unmarshal(route.Body, &bodyMap); err == nil {
v0Route.Body = bodyMap
}
}
meta.Routes = append(meta.Routes, v0Route)
}
}
// Map Extensions
if len(jsonData.Extensions.AddedLinks) > 0 || len(jsonData.Extensions.AddedComponents) > 0 ||
len(jsonData.Extensions.ExposedComponents) > 0 || len(jsonData.Extensions.ExtensionPoints) > 0 {
extensions := &pluginsv0alpha1.MetaExtensions{}
if len(jsonData.Extensions.AddedLinks) > 0 {
extensions.AddedLinks = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks, 0, len(jsonData.Extensions.AddedLinks))
for _, link := range jsonData.Extensions.AddedLinks {
v0Link := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedLinks{
Targets: link.Targets,
Title: link.Title,
}
if link.Description != "" {
v0Link.Description = &link.Description
}
extensions.AddedLinks = append(extensions.AddedLinks, v0Link)
}
}
if len(jsonData.Extensions.AddedComponents) > 0 {
extensions.AddedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents, 0, len(jsonData.Extensions.AddedComponents))
for _, comp := range jsonData.Extensions.AddedComponents {
v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsAddedComponents{
Targets: comp.Targets,
Title: comp.Title,
}
if comp.Description != "" {
v0Comp.Description = &comp.Description
}
extensions.AddedComponents = append(extensions.AddedComponents, v0Comp)
}
}
if len(jsonData.Extensions.ExposedComponents) > 0 {
extensions.ExposedComponents = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents, 0, len(jsonData.Extensions.ExposedComponents))
for _, comp := range jsonData.Extensions.ExposedComponents {
v0Comp := pluginsv0alpha1.MetaV0alpha1ExtensionsExposedComponents{
Id: comp.Id,
}
if comp.Title != "" {
v0Comp.Title = &comp.Title
}
if comp.Description != "" {
v0Comp.Description = &comp.Description
}
extensions.ExposedComponents = append(extensions.ExposedComponents, v0Comp)
}
}
if len(jsonData.Extensions.ExtensionPoints) > 0 {
extensions.ExtensionPoints = make([]pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints, 0, len(jsonData.Extensions.ExtensionPoints))
for _, point := range jsonData.Extensions.ExtensionPoints {
v0Point := pluginsv0alpha1.MetaV0alpha1ExtensionsExtensionPoints{
Id: point.Id,
}
if point.Title != "" {
v0Point.Title = &point.Title
}
if point.Description != "" {
v0Point.Description = &point.Description
}
extensions.ExtensionPoints = append(extensions.ExtensionPoints, v0Point)
}
}
meta.Extensions = extensions
}
// Map Roles
if len(jsonData.Roles) > 0 {
meta.Roles = make([]pluginsv0alpha1.MetaRole, 0, len(jsonData.Roles))
for _, role := range jsonData.Roles {
v0Role := pluginsv0alpha1.MetaRole{
Grants: role.Grants,
}
if role.Role.Name != "" || role.Role.Description != "" || len(role.Role.Permissions) > 0 {
v0RoleRole := &pluginsv0alpha1.MetaV0alpha1RoleRole{}
if role.Role.Name != "" {
v0RoleRole.Name = &role.Role.Name
}
if role.Role.Description != "" {
v0RoleRole.Description = &role.Role.Description
}
if len(role.Role.Permissions) > 0 {
v0RoleRole.Permissions = make([]pluginsv0alpha1.MetaV0alpha1RoleRolePermissions, 0, len(role.Role.Permissions))
for _, perm := range role.Role.Permissions {
v0Perm := pluginsv0alpha1.MetaV0alpha1RoleRolePermissions{}
if perm.Action != "" {
v0Perm.Action = &perm.Action
}
if perm.Scope != "" {
v0Perm.Scope = &perm.Scope
}
v0RoleRole.Permissions = append(v0RoleRole.Permissions, v0Perm)
}
}
v0Role.Role = v0RoleRole
}
meta.Roles = append(meta.Roles, v0Role)
}
}
// Map IAM
if jsonData.IAM != nil && len(jsonData.IAM.Permissions) > 0 {
iam := &pluginsv0alpha1.MetaIAM{
Permissions: make([]pluginsv0alpha1.MetaV0alpha1IAMPermissions, 0, len(jsonData.IAM.Permissions)),
}
for _, perm := range jsonData.IAM.Permissions {
v0Perm := pluginsv0alpha1.MetaV0alpha1IAMPermissions{}
if perm.Action != "" {
v0Perm.Action = &perm.Action
}
if perm.Scope != "" {
v0Perm.Scope = &perm.Scope
}
iam.Permissions = append(iam.Permissions, v0Perm)
}
meta.Iam = iam
}
return meta
}

View File

@@ -22,12 +22,10 @@ func TestCoreProvider_GetMeta(t *testing.T) {
t.Run("returns cached plugin when available", func(t *testing.T) {
provider := NewCoreProvider()
expectedMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
provider.mu.Lock()
@@ -60,12 +58,10 @@ func TestCoreProvider_GetMeta(t *testing.T) {
t.Run("ignores version parameter", func(t *testing.T) {
provider := NewCoreProvider()
expectedMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
provider.mu.Lock()
@@ -85,12 +81,10 @@ func TestCoreProvider_GetMeta(t *testing.T) {
customTTL := 2 * time.Hour
provider := NewCoreProviderWithTTL(customTTL)
expectedMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
provider.mu.Lock()
@@ -232,8 +226,8 @@ func TestCoreProvider_loadPlugins(t *testing.T) {
if loaded {
result, err := provider.GetMeta(ctx, "test-datasource", "1.0.0")
require.NoError(t, err)
assert.Equal(t, "test-datasource", result.Meta.PluginJson.Id)
assert.Equal(t, "Test Datasource", result.Meta.PluginJson.Name)
assert.Equal(t, "test-datasource", result.Meta.Id)
assert.Equal(t, "Test Datasource", result.Meta.Name)
}
})
}

View File

@@ -1,53 +0,0 @@
package meta
import (
"context"
"time"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
const (
defaultLocalTTL = 1 * time.Hour
)
// PluginAssetsCalculator is an interface for calculating plugin asset information.
// LocalProvider requires this to calculate loading strategy and module hash.
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.
// It uses the plugin store to access plugins that have already been loaded.
type LocalProvider struct {
store pluginstore.Store
pluginAssets PluginAssetsCalculator
}
// NewLocalProvider creates a new LocalProvider for locally installed plugins.
// pluginAssets is required for calculating loading strategy and module hash.
func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider {
return &LocalProvider{
store: pluginStore,
pluginAssets: pluginAssets,
}
}
// GetMeta retrieves plugin metadata for locally installed plugins.
func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) (*Result, error) {
plugin, exists := p.store.Plugin(ctx, pluginID)
if !exists {
return nil, ErrMetaNotFound
}
loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin)
moduleHash := p.pluginAssets.ModuleHash(ctx, plugin)
spec := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash)
return &Result{
Meta: spec,
TTL: defaultLocalTTL,
}, nil
}

View File

@@ -16,7 +16,7 @@ const (
// cachedMeta represents a cached metadata entry with expiration time
type cachedMeta struct {
meta pluginsv0alpha1.MetaSpec
meta pluginsv0alpha1.MetaJSONData
ttl time.Duration
expiresAt time.Time
}
@@ -84,7 +84,7 @@ func (pm *ProviderManager) GetMeta(ctx context.Context, pluginID, version string
if err == nil {
// Don't cache results with a zero TTL
if result.TTL == 0 {
return result, nil
continue
}
pm.cacheMu.Lock()

View File

@@ -35,12 +35,10 @@ func TestProviderManager_GetMeta(t *testing.T) {
ctx := context.Background()
t.Run("returns cached result when available and not expired", func(t *testing.T) {
cachedMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
cachedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
provider := &mockProvider{
@@ -62,10 +60,8 @@ func TestProviderManager_GetMeta(t *testing.T) {
provider.getMetaFunc = func(ctx context.Context, pluginID, version string) (*Result, error) {
return &Result{
Meta: pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{Id: "different"},
},
TTL: time.Hour,
Meta: pluginsv0alpha1.MetaJSONData{Id: "different"},
TTL: time.Hour,
}, nil
}
@@ -77,12 +73,10 @@ func TestProviderManager_GetMeta(t *testing.T) {
})
t.Run("fetches from provider when not cached", func(t *testing.T) {
expectedMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
expectedTTL := 2 * time.Hour
@@ -113,16 +107,19 @@ func TestProviderManager_GetMeta(t *testing.T) {
assert.Equal(t, expectedTTL, cached.ttl)
})
t.Run("does not cache result with zero TTL", func(t *testing.T) {
zeroTTLMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Zero TTL Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
t.Run("does not cache result with zero TTL and tries next provider", func(t *testing.T) {
zeroTTLMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Zero TTL Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
expectedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
provider := &mockProvider{
provider1 := &mockProvider{
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
return &Result{
Meta: zeroTTLMeta,
@@ -130,30 +127,37 @@ func TestProviderManager_GetMeta(t *testing.T) {
}, nil
},
}
provider2 := &mockProvider{
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {
return &Result{
Meta: expectedMeta,
TTL: time.Hour,
}, nil
},
}
pm := NewProviderManager(provider)
pm := NewProviderManager(provider1, provider2)
result, err := pm.GetMeta(ctx, "test-plugin", "1.0.0")
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, zeroTTLMeta, result.Meta)
assert.Equal(t, time.Duration(0), result.TTL)
assert.Equal(t, expectedMeta, result.Meta)
pm.cacheMu.RLock()
_, exists := pm.cache["test-plugin:1.0.0"]
cached, exists := pm.cache["test-plugin:1.0.0"]
pm.cacheMu.RUnlock()
assert.False(t, exists, "zero TTL results should not be cached")
assert.True(t, exists)
assert.Equal(t, expectedMeta, cached.meta)
assert.Equal(t, time.Hour, cached.ttl)
})
t.Run("tries next provider when first returns ErrMetaNotFound", func(t *testing.T) {
expectedMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
provider1 := &mockProvider{
@@ -225,19 +229,15 @@ func TestProviderManager_GetMeta(t *testing.T) {
})
t.Run("skips expired cache entries", func(t *testing.T) {
expiredMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Expired Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expiredMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Expired Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
expectedMeta := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Test Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
callCount := 0
@@ -272,19 +272,15 @@ func TestProviderManager_GetMeta(t *testing.T) {
})
t.Run("uses first successful provider", func(t *testing.T) {
expectedMeta1 := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Provider 1 Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta1 := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Provider 1 Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
expectedMeta2 := pluginsv0alpha1.MetaSpec{
PluginJson: pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Provider 2 Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
},
expectedMeta2 := pluginsv0alpha1.MetaJSONData{
Id: "test-plugin",
Name: "Provider 2 Plugin",
Type: pluginsv0alpha1.MetaJSONDataTypeDatasource,
}
provider1 := &mockProvider{
@@ -335,9 +331,9 @@ func TestProviderManager_Run(t *testing.T) {
func TestProviderManager_cleanupExpired(t *testing.T) {
t.Run("removes expired entries", func(t *testing.T) {
validMeta := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "valid"}}
expiredMeta1 := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "expired1"}}
expiredMeta2 := pluginsv0alpha1.MetaSpec{PluginJson: pluginsv0alpha1.MetaJSONData{Id: "expired2"}}
validMeta := pluginsv0alpha1.MetaJSONData{Id: "valid"}
expiredMeta1 := pluginsv0alpha1.MetaJSONData{Id: "expired1"}
expiredMeta2 := pluginsv0alpha1.MetaJSONData{Id: "expired2"}
provider := &mockProvider{
getMetaFunc: func(ctx context.Context, pluginID, version string) (*Result, error) {

View File

@@ -14,7 +14,7 @@ var (
// Result contains plugin metadata along with its recommended TTL.
type Result struct {
Meta pluginsv0alpha1.MetaSpec
Meta pluginsv0alpha1.MetaJSONData
TTL time.Duration
}

View File

@@ -121,19 +121,8 @@ func (s *MetaStorage) List(ctx context.Context, options *internalversion.ListOpt
continue
}
pluginMeta := pluginsv0alpha1.Meta{
ObjectMeta: metav1.ObjectMeta{
Name: plugin.Name,
Namespace: plugin.Namespace,
},
Spec: result.Meta,
}
pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{
Group: pluginsv0alpha1.APIGroup,
Version: pluginsv0alpha1.APIVersion,
Kind: pluginsv0alpha1.MetaKind().Kind(),
})
metaItems = append(metaItems, pluginMeta)
pluginMeta := createMetaFromMetaJSONData(result.Meta, plugin.Name, plugin.Namespace)
metaItems = append(metaItems, *pluginMeta)
}
list := &pluginsv0alpha1.MetaList{
@@ -180,18 +169,27 @@ func (s *MetaStorage) Get(ctx context.Context, name string, options *metav1.GetO
return nil, apierrors.NewInternalError(fmt.Errorf("failed to fetch plugin metadata: %w", err))
}
return createMetaFromMetaJSONData(result.Meta, name, ns.Value), nil
}
// createMetaFromMetaJSONData creates a Meta k8s object from MetaJSONData and plugin metadata.
func createMetaFromMetaJSONData(pluginJSON pluginsv0alpha1.MetaJSONData, name, namespace string) *pluginsv0alpha1.Meta {
pluginMeta := &pluginsv0alpha1.Meta{
ObjectMeta: metav1.ObjectMeta{
Name: plugin.Name,
Namespace: plugin.Namespace,
Name: name,
Namespace: namespace,
},
Spec: pluginsv0alpha1.MetaSpec{
PluginJSON: pluginJSON,
},
Spec: result.Meta,
}
// Set the GroupVersionKind
pluginMeta.SetGroupVersionKind(schema.GroupVersionKind{
Group: pluginsv0alpha1.APIGroup,
Version: pluginsv0alpha1.APIVersion,
Kind: pluginsv0alpha1.MetaKind().Kind(),
})
return pluginMeta, nil
return pluginMeta
}

View File

@@ -7,7 +7,6 @@ require (
github.com/google/go-github/v70 v70.0.0
github.com/google/uuid v1.6.0
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f
github.com/grafana/grafana-app-sdk v0.48.7
github.com/grafana/grafana-app-sdk/logging v0.48.7
github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2
@@ -29,7 +28,6 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -56,20 +54,13 @@ require (
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/grafana/grafana-app-sdk v0.48.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -77,7 +68,6 @@ require (
github.com/prometheus/procfs v0.19.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect

View File

@@ -14,8 +14,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -61,8 +59,6 @@ github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -102,13 +98,6 @@ github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4=
github.com/grafana/nanogit v0.3.0 h1:XNEef+4Vi+465ZITJs/g/xgnDRJbWhhJ7iQrAnWZ0oQ=
github.com/grafana/nanogit v0.3.0/go.mod h1:6s6CCTpyMOHPpcUZaLGI+rgBEKdmxVbhqSGgCK13j7Y=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -121,8 +110,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -131,22 +118,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -171,10 +150,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@@ -1,10 +1,9 @@
package repository
manifest: {
appName: "provisioning"
groupOverride: "provisioning.grafana.app"
preferredVersion: "v0alpha1"
kinds: [
appName: "provisioning"
groupOverride: "provisioning.grafana.app"
kinds: [
repository,
connection
]

View File

@@ -80,7 +80,7 @@ repository: {
// Enabled must be saved as true before any sync job will run
enabled: bool
// Where values should be saved
target: "instance" | "folder"
target: "unified" | "legacy"
// When non-zero, the sync will run periodically
intervalSeconds?: int
}

View File

@@ -1,92 +0,0 @@
//
// This file is generated by grafana-app-sdk
// DO NOT EDIT
//
package manifestdata
import (
"fmt"
"strings"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/resource"
"k8s.io/apimachinery/pkg/runtime"
)
var appManifestData = app.ManifestData{
AppName: "provisioning",
Group: "provisioning.grafana.app",
PreferredVersion: "v0alpha1",
Versions: []app.ManifestVersion{},
}
func LocalManifest() app.Manifest {
return app.NewEmbeddedManifest(appManifestData)
}
func RemoteManifest() app.Manifest {
return app.NewAPIServerManifest("provisioning")
}
var kindVersionToGoType = map[string]resource.Kind{}
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
// If there is no association for the provided Kind and Version, exists will return false.
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
return goType, exists
}
var customRouteToGoResponseType = map[string]any{}
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
// If there is no association for the provided kind, version, custom route path, and method, exists will return false.
// Resource routes (those without a kind) should prefix their route with "<namespace>/" if the route is namespaced (otherwise the route is assumed to be cluster-scope)
func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoParamsType = map[string]runtime.Object{}
func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoRequestBodyType = map[string]any{}
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
type GoTypeAssociator struct{}
func NewGoTypeAssociator() *GoTypeAssociator {
return &GoTypeAssociator{}
}
func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) {
return ManifestGoTypeAssociator(kind, version)
}
func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteResponsesAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) {
return ManifestCustomRouteQueryAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb)
}

View File

@@ -136,6 +136,9 @@ type ExportJobOptions struct {
}
type MigrateJobOptions struct {
// Preserve history (if possible)
History bool `json:"history,omitempty"`
// Message to use when committing the changes in a single commit
Message string `json:"message,omitempty"`
}

View File

@@ -9,6 +9,11 @@ import (
type RepositoryViewList struct {
metav1.TypeMeta `json:",inline"`
// The backend is using legacy storage
// FIXME: Not sure where this should be exposed... but we need it somewhere
// The UI should force the onboarding workflow when this is true
LegacyStorage bool `json:"legacyStorage,omitempty"`
// The valid targets (can disable instance or folder types)
AllowedTargets []SyncTargetType `json:"allowedTargets,omitempty"`

View File

@@ -1495,6 +1495,13 @@ func schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref common.Referenc
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"history": {
SchemaProps: spec.SchemaProps{
Description: "Preserve history (if possible)",
Type: []string{"boolean"},
Format: "",
},
},
"message": {
SchemaProps: spec.SchemaProps{
Description: "Message to use when committing the changes in a single commit",
@@ -2112,6 +2119,13 @@ func schema_pkg_apis_provisioning_v0alpha1_RepositoryViewList(ref common.Referen
Format: "",
},
},
"legacyStorage": {
SchemaProps: spec.SchemaProps{
Description: "The backend is using legacy storage FIXME: Not sure where this should be exposed... but we need it somewhere The UI should force the onboarding workflow when this is true",
Type: []string{"boolean"},
Format: "",
},
},
"allowedTargets": {
SchemaProps: spec.SchemaProps{
Description: "The valid targets (can disable instance or folder types)",

View File

@@ -1,22 +0,0 @@
package auth
import (
"context"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
//go:generate mockery --name AccessChecker --structname MockAccessChecker --inpackage --filename access_checker_mock.go --with-expecter
// AccessChecker provides access control checks with optional role-based fallback.
type AccessChecker interface {
// Check performs an access check and returns nil if allowed, or an appropriate
// API error if denied. If req.Namespace is empty, it will be filled from the
// identity's namespace.
Check(ctx context.Context, req authlib.CheckRequest, folder string) error
// WithFallbackRole returns an AccessChecker configured with the specified fallback role.
// Whether the fallback is actually applied depends on the implementation.
WithFallbackRole(role identity.RoleType) AccessChecker
}

View File

@@ -1,135 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package auth
import (
context "context"
identity "github.com/grafana/grafana/pkg/apimachinery/identity"
mock "github.com/stretchr/testify/mock"
types "github.com/grafana/authlib/types"
)
// MockAccessChecker is an autogenerated mock type for the AccessChecker type
type MockAccessChecker struct {
mock.Mock
}
type MockAccessChecker_Expecter struct {
mock *mock.Mock
}
func (_m *MockAccessChecker) EXPECT() *MockAccessChecker_Expecter {
return &MockAccessChecker_Expecter{mock: &_m.Mock}
}
// Check provides a mock function with given fields: ctx, req, folder
func (_m *MockAccessChecker) Check(ctx context.Context, req types.CheckRequest, folder string) error {
ret := _m.Called(ctx, req, folder)
if len(ret) == 0 {
panic("no return value specified for Check")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, types.CheckRequest, string) error); ok {
r0 = rf(ctx, req, folder)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockAccessChecker_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check'
type MockAccessChecker_Check_Call struct {
*mock.Call
}
// Check is a helper method to define mock.On call
// - ctx context.Context
// - req types.CheckRequest
// - folder string
func (_e *MockAccessChecker_Expecter) Check(ctx interface{}, req interface{}, folder interface{}) *MockAccessChecker_Check_Call {
return &MockAccessChecker_Check_Call{Call: _e.mock.On("Check", ctx, req, folder)}
}
func (_c *MockAccessChecker_Check_Call) Run(run func(ctx context.Context, req types.CheckRequest, folder string)) *MockAccessChecker_Check_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(types.CheckRequest), args[2].(string))
})
return _c
}
func (_c *MockAccessChecker_Check_Call) Return(_a0 error) *MockAccessChecker_Check_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockAccessChecker_Check_Call) RunAndReturn(run func(context.Context, types.CheckRequest, string) error) *MockAccessChecker_Check_Call {
_c.Call.Return(run)
return _c
}
// WithFallbackRole provides a mock function with given fields: role
func (_m *MockAccessChecker) WithFallbackRole(role identity.RoleType) AccessChecker {
ret := _m.Called(role)
if len(ret) == 0 {
panic("no return value specified for WithFallbackRole")
}
var r0 AccessChecker
if rf, ok := ret.Get(0).(func(identity.RoleType) AccessChecker); ok {
r0 = rf(role)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(AccessChecker)
}
}
return r0
}
// MockAccessChecker_WithFallbackRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithFallbackRole'
type MockAccessChecker_WithFallbackRole_Call struct {
*mock.Call
}
// WithFallbackRole is a helper method to define mock.On call
// - role identity.RoleType
func (_e *MockAccessChecker_Expecter) WithFallbackRole(role interface{}) *MockAccessChecker_WithFallbackRole_Call {
return &MockAccessChecker_WithFallbackRole_Call{Call: _e.mock.On("WithFallbackRole", role)}
}
func (_c *MockAccessChecker_WithFallbackRole_Call) Run(run func(role identity.RoleType)) *MockAccessChecker_WithFallbackRole_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(identity.RoleType))
})
return _c
}
func (_c *MockAccessChecker_WithFallbackRole_Call) Return(_a0 AccessChecker) *MockAccessChecker_WithFallbackRole_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockAccessChecker_WithFallbackRole_Call) RunAndReturn(run func(identity.RoleType) AccessChecker) *MockAccessChecker_WithFallbackRole_Call {
_c.Call.Return(run)
return _c
}
// NewMockAccessChecker creates a new instance of MockAccessChecker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockAccessChecker(t interface {
mock.TestingT
Cleanup(func())
}) *MockAccessChecker {
mock := &MockAccessChecker{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,4 +1,3 @@
// Package auth provides authentication utilities for the provisioning API.
package auth
import (
@@ -7,6 +6,7 @@ import (
"net/http"
"github.com/grafana/authlib/authn"
"github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
utilnet "k8s.io/apimachinery/pkg/util/net"
)
@@ -15,61 +15,29 @@ type tokenExchanger interface {
Exchange(ctx context.Context, req authn.TokenExchangeRequest) (*authn.TokenExchangeResponse, error)
}
// RoundTripperOption configures optional behavior for the RoundTripper.
type RoundTripperOption func(*RoundTripper)
// ExtraAudience appends an additional audience to the token exchange request.
//
// This is primarily used by operators connecting to the multitenant aggregator,
// where the token must include both the target API server's audience (e.g., dashboards,
// folders) and the provisioning group audience. The provisioning group audience is
// required so that the token passes the enforceManagerProperties check, which prevents
// unauthorized updates to provisioned resources.
//
// Example:
//
// authrt.NewRoundTripper(client, rt, "dashboards.grafana.app", authrt.ExtraAudience("provisioning.grafana.app"))
func ExtraAudience(audience string) RoundTripperOption {
return func(rt *RoundTripper) {
rt.extraAudience = audience
}
}
// RoundTripper is an http.RoundTripper that performs token exchange before each request.
// It exchanges the service's credentials for an access token scoped to the configured
// audience(s), then injects that token into the outgoing request's X-Access-Token header.
// RoundTripper injects an exchanged access token for the provisioning API into outgoing requests.
type RoundTripper struct {
client tokenExchanger
transport http.RoundTripper
audience string
extraAudience string
client tokenExchanger
transport http.RoundTripper
audience string
}
// NewRoundTripper creates a RoundTripper that exchanges tokens for each outgoing request.
//
// Parameters:
// - tokenExchangeClient: the client used to exchange credentials for access tokens
// - base: the underlying transport to delegate requests to after token injection
// - audience: the primary audience for the token (typically the target API server's group)
// - opts: optional configuration (e.g., ExtraAudience to include additional audiences)
func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper, audience string, opts ...RoundTripperOption) *RoundTripper {
rt := &RoundTripper{
// NewRoundTripper constructs a RoundTripper that exchanges the provided token per request
// and forwards the request to the provided base transport.
func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper, audience string) *RoundTripper {
return &RoundTripper{
client: tokenExchangeClient,
transport: base,
audience: audience,
}
for _, opt := range opts {
opt(rt)
}
return rt
}
// RoundTrip exchanges credentials for an access token and injects it into the request.
// The token is scoped to all configured audiences and the wildcard namespace ("*").
func (t *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// when we want to write resources with the provisioning API, the audience needs to include provisioning
// so that it passes the check in enforceManagerProperties, which prevents others from updating provisioned resources
audiences := []string{t.audience}
if t.extraAudience != "" && t.extraAudience != t.audience {
audiences = append(audiences, t.extraAudience)
if t.audience != v0alpha1.GROUP {
audiences = append(audiences, v0alpha1.GROUP)
}
tokenResponse, err := t.client.Exchange(req.Context(), authn.TokenExchangeRequest{

View File

@@ -71,29 +71,16 @@ func TestRoundTripper_AudiencesAndNamespace(t *testing.T) {
tests := []struct {
name string
audience string
extraAudience string
wantAudiences []string
}{
{
name: "uses only provided audience by default",
name: "adds group when custom audience",
audience: "example-audience",
wantAudiences: []string{"example-audience"},
},
{
name: "uses only group audience by default",
audience: v0alpha1.GROUP,
wantAudiences: []string{v0alpha1.GROUP},
},
{
name: "extra audience adds provisioning group",
audience: "example-audience",
extraAudience: v0alpha1.GROUP,
wantAudiences: []string{"example-audience", v0alpha1.GROUP},
},
{
name: "extra audience no duplicate when same as primary",
name: "no duplicate when group audience",
audience: v0alpha1.GROUP,
extraAudience: v0alpha1.GROUP,
wantAudiences: []string{v0alpha1.GROUP},
},
}
@@ -101,15 +88,11 @@ func TestRoundTripper_AudiencesAndNamespace(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fx := &fakeExchanger{resp: &authn.TokenExchangeResponse{Token: "abc123"}}
var opts []RoundTripperOption
if tt.extraAudience != "" {
opts = append(opts, ExtraAudience(tt.extraAudience))
}
tr := NewRoundTripper(fx, roundTripperFunc(func(_ *http.Request) (*http.Response, error) {
rr := httptest.NewRecorder()
rr.WriteHeader(http.StatusOK)
return rr.Result(), nil
}), tt.audience, opts...)
}), tt.audience)
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example", nil)
resp, err := tr.RoundTrip(req)

View File

@@ -1,153 +0,0 @@
package auth
import (
"context"
"fmt"
"strings"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
// sessionAccessChecker implements AccessChecker using Grafana session identity.
type sessionAccessChecker struct {
inner authlib.AccessChecker
fallbackRole identity.RoleType
}
// NewSessionAccessChecker creates an AccessChecker that gets identity from Grafana
// sessions via GetRequester(ctx). Supports optional role-based fallback via
// WithFallbackRole for backwards compatibility.
func NewSessionAccessChecker(inner authlib.AccessChecker) AccessChecker {
return &sessionAccessChecker{
inner: inner,
fallbackRole: "",
}
}
// WithFallbackRole returns a new AccessChecker with the specified fallback role.
func (c *sessionAccessChecker) WithFallbackRole(role identity.RoleType) AccessChecker {
return &sessionAccessChecker{
inner: c.inner,
fallbackRole: role,
}
}
// Check performs an access check with optional role-based fallback.
// Returns nil if access is allowed, or an appropriate API error if denied.
func (c *sessionAccessChecker) Check(ctx context.Context, req authlib.CheckRequest, folder string) error {
logger := logging.FromContext(ctx).With("logger", "sessionAccessChecker")
// Get identity from Grafana session
requester, err := identity.GetRequester(ctx)
if err != nil {
logger.Debug("failed to get requester",
"resource", req.Resource,
"verb", req.Verb,
"error", err.Error(),
)
return apierrors.NewUnauthorized(fmt.Sprintf("failed to get requester: %v", err))
}
logger.Debug("checking access",
"identityType", requester.GetIdentityType(),
"orgRole", requester.GetOrgRole(),
"namespace", requester.GetNamespace(),
"resource", req.Resource,
"verb", req.Verb,
"group", req.Group,
"name", req.Name,
"folder", folder,
"fallbackRole", c.fallbackRole,
)
// Fill in namespace from identity if not provided
if req.Namespace == "" {
req.Namespace = requester.GetNamespace()
}
// Perform the access check
rsp, err := c.inner.Check(ctx, requester, req, folder)
// Build the GroupResource for error messages
gr := schema.GroupResource{Group: req.Group, Resource: req.Resource}
// No fallback configured, return result directly
if c.fallbackRole == "" {
if err != nil {
logger.Debug("access check error (no fallback)",
"resource", req.Resource,
"verb", req.Verb,
"error", err.Error(),
)
return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s.%s is forbidden: %w", req.Resource, req.Group, err))
}
if !rsp.Allowed {
logger.Debug("access check denied (no fallback)",
"resource", req.Resource,
"verb", req.Verb,
"group", req.Group,
"allowed", rsp.Allowed,
)
return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("permission denied"))
}
logger.Debug("access allowed",
"resource", req.Resource,
"verb", req.Verb,
)
return nil
}
// Fallback is configured - apply fallback logic
if err != nil {
if requester.GetOrgRole().Includes(c.fallbackRole) {
logger.Debug("access allowed via role fallback (after error)",
"resource", req.Resource,
"verb", req.Verb,
"fallbackRole", c.fallbackRole,
"orgRole", requester.GetOrgRole(),
)
return nil // Fallback succeeded
}
logger.Debug("access check error (fallback failed)",
"resource", req.Resource,
"verb", req.Verb,
"error", err.Error(),
"fallbackRole", c.fallbackRole,
"orgRole", requester.GetOrgRole(),
)
return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s.%s is forbidden: %w", req.Resource, req.Group, err))
}
if rsp.Allowed {
logger.Debug("access allowed",
"resource", req.Resource,
"verb", req.Verb,
)
return nil
}
// Fall back to role for backwards compatibility
if requester.GetOrgRole().Includes(c.fallbackRole) {
logger.Debug("access allowed via role fallback",
"resource", req.Resource,
"verb", req.Verb,
"fallbackRole", c.fallbackRole,
"orgRole", requester.GetOrgRole(),
)
return nil // Fallback succeeded
}
logger.Debug("access denied (fallback role not met)",
"resource", req.Resource,
"verb", req.Verb,
"group", req.Group,
"fallbackRole", c.fallbackRole,
"orgRole", requester.GetOrgRole(),
)
return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s role is required", strings.ToLower(string(c.fallbackRole))))
}

View File

@@ -1,244 +0,0 @@
package auth
import (
"context"
"errors"
"testing"
apierrors "k8s.io/apimachinery/pkg/api/errors"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockRequester implements identity.Requester for testing.
type mockRequester struct {
identity.Requester
orgRole identity.RoleType
identityType authlib.IdentityType
namespace string
}
func (m *mockRequester) GetOrgRole() identity.RoleType {
return m.orgRole
}
func (m *mockRequester) GetIdentityType() authlib.IdentityType {
return m.identityType
}
func (m *mockRequester) GetNamespace() string {
return m.namespace
}
func TestSessionAccessChecker_Check(t *testing.T) {
ctx := context.Background()
req := authlib.CheckRequest{
Verb: "get",
Group: "provisioning.grafana.app",
Resource: "repositories",
Name: "test-repo",
Namespace: "default",
}
tests := []struct {
name string
fallbackRole identity.RoleType
innerResponse authlib.CheckResponse
innerErr error
requester *mockRequester
expectAllow bool
}{
{
name: "allowed by checker",
fallbackRole: identity.RoleAdmin,
innerResponse: authlib.CheckResponse{Allowed: true},
requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser},
expectAllow: true,
},
{
name: "denied by checker, fallback to admin role succeeds",
fallbackRole: identity.RoleAdmin,
innerResponse: authlib.CheckResponse{Allowed: false},
requester: &mockRequester{orgRole: identity.RoleAdmin, identityType: authlib.TypeUser},
expectAllow: true,
},
{
name: "denied by checker, fallback to admin role fails for viewer",
fallbackRole: identity.RoleAdmin,
innerResponse: authlib.CheckResponse{Allowed: false},
requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser},
expectAllow: false,
},
{
name: "error from checker, fallback to admin role succeeds",
fallbackRole: identity.RoleAdmin,
innerErr: errors.New("access check failed"),
requester: &mockRequester{orgRole: identity.RoleAdmin, identityType: authlib.TypeUser},
expectAllow: true,
},
{
name: "error from checker, fallback fails for viewer",
fallbackRole: identity.RoleAdmin,
innerErr: errors.New("access check failed"),
requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser},
expectAllow: false,
},
{
name: "denied, editor fallback succeeds for editor",
fallbackRole: identity.RoleEditor,
innerResponse: authlib.CheckResponse{Allowed: false},
requester: &mockRequester{orgRole: identity.RoleEditor, identityType: authlib.TypeUser},
expectAllow: true,
},
{
name: "denied, editor fallback fails for viewer",
fallbackRole: identity.RoleEditor,
innerResponse: authlib.CheckResponse{Allowed: false},
requester: &mockRequester{orgRole: identity.RoleViewer, identityType: authlib.TypeUser},
expectAllow: false,
},
{
name: "no fallback configured, denied stays denied",
fallbackRole: "", // no fallback
innerResponse: authlib.CheckResponse{Allowed: false},
requester: &mockRequester{orgRole: identity.RoleAdmin, identityType: authlib.TypeUser},
expectAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &mockInnerAccessChecker{
response: tt.innerResponse,
err: tt.innerErr,
}
checker := NewSessionAccessChecker(mock)
if tt.fallbackRole != "" {
checker = checker.WithFallbackRole(tt.fallbackRole)
}
// Add requester to context
testCtx := identity.WithRequester(ctx, tt.requester)
err := checker.Check(testCtx, req, "")
if tt.expectAllow {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.True(t, apierrors.IsForbidden(err), "expected Forbidden error, got: %v", err)
}
})
}
}
func TestSessionAccessChecker_NoRequester(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: true},
}
checker := NewSessionAccessChecker(mock)
err := checker.Check(context.Background(), authlib.CheckRequest{}, "")
require.Error(t, err)
assert.True(t, apierrors.IsUnauthorized(err), "expected Unauthorized error")
}
func TestSessionAccessChecker_WithFallbackRole_ImmutableOriginal(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: false},
}
original := NewSessionAccessChecker(mock)
withAdmin := original.WithFallbackRole(identity.RoleAdmin)
withEditor := original.WithFallbackRole(identity.RoleEditor)
ctx := identity.WithRequester(context.Background(), &mockRequester{
orgRole: identity.RoleEditor,
identityType: authlib.TypeUser,
})
req := authlib.CheckRequest{}
// Original should deny (no fallback)
err := original.Check(ctx, req, "")
require.Error(t, err, "original should deny without fallback")
// WithAdmin should deny for editor
err = withAdmin.Check(ctx, req, "")
require.Error(t, err, "admin fallback should deny for editor")
// WithEditor should allow for editor
err = withEditor.Check(ctx, req, "")
require.NoError(t, err, "editor fallback should allow for editor")
}
func TestSessionAccessChecker_WithFallbackRole_ChainedCalls(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: false},
}
// Ensure chained WithFallbackRole calls work correctly
checker := NewSessionAccessChecker(mock).
WithFallbackRole(identity.RoleAdmin).
WithFallbackRole(identity.RoleEditor) // This should override admin
ctx := identity.WithRequester(context.Background(), &mockRequester{
orgRole: identity.RoleEditor,
identityType: authlib.TypeUser,
})
err := checker.Check(ctx, authlib.CheckRequest{}, "")
require.NoError(t, err, "last fallback (editor) should be used")
}
func TestSessionAccessChecker_RealSignedInUser(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: false},
}
checker := NewSessionAccessChecker(mock).WithFallbackRole(identity.RoleAdmin)
// Use a real SignedInUser
signedInUser := &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: identity.RoleAdmin,
}
ctx := identity.WithRequester(context.Background(), signedInUser)
err := checker.Check(ctx, authlib.CheckRequest{}, "")
require.NoError(t, err, "admin user should be allowed via fallback")
}
func TestSessionAccessChecker_FillsNamespace(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: true},
}
checker := NewSessionAccessChecker(mock)
ctx := identity.WithRequester(context.Background(), &mockRequester{
orgRole: identity.RoleAdmin,
identityType: authlib.TypeUser,
namespace: "org-123",
})
// Request without namespace
req := authlib.CheckRequest{
Verb: "get",
Group: "provisioning.grafana.app",
Resource: "repositories",
Name: "test-repo",
// Namespace intentionally empty
}
err := checker.Check(ctx, req, "")
require.NoError(t, err)
}

View File

@@ -1,92 +0,0 @@
package auth
import (
"context"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
// tokenAccessChecker implements AccessChecker using access tokens from context.
type tokenAccessChecker struct {
inner authlib.AccessChecker
}
// NewTokenAccessChecker creates an AccessChecker that gets identity from access tokens
// via AuthInfoFrom(ctx). Role-based fallback is not supported.
func NewTokenAccessChecker(inner authlib.AccessChecker) AccessChecker {
return &tokenAccessChecker{inner: inner}
}
// WithFallbackRole returns the same checker since fallback is not supported.
func (c *tokenAccessChecker) WithFallbackRole(_ identity.RoleType) AccessChecker {
return c
}
// Check performs an access check using AuthInfo from context.
// Returns nil if access is allowed, or an appropriate API error if denied.
func (c *tokenAccessChecker) Check(ctx context.Context, req authlib.CheckRequest, folder string) error {
logger := logging.FromContext(ctx).With("logger", "tokenAccessChecker")
// Get identity from access token in context
id, ok := authlib.AuthInfoFrom(ctx)
if !ok {
logger.Debug("no auth info in context",
"resource", req.Resource,
"verb", req.Verb,
"namespace", req.Namespace,
)
return apierrors.NewUnauthorized("no auth info in context")
}
logger.Debug("checking access",
"identityType", id.GetIdentityType(),
"namespace", id.GetNamespace(),
"resource", req.Resource,
"verb", req.Verb,
"group", req.Group,
"name", req.Name,
"folder", folder,
)
// Fill in namespace from identity if not provided
if req.Namespace == "" {
req.Namespace = id.GetNamespace()
}
// Perform the access check
rsp, err := c.inner.Check(ctx, id, req, folder)
// Build the GroupResource for error messages
gr := schema.GroupResource{Group: req.Group, Resource: req.Resource}
if err != nil {
logger.Debug("access check error",
"resource", req.Resource,
"verb", req.Verb,
"error", err.Error(),
)
return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("%s.%s is forbidden: %w", req.Resource, req.Group, err))
}
if !rsp.Allowed {
logger.Debug("access check denied",
"resource", req.Resource,
"verb", req.Verb,
"group", req.Group,
"identityType", id.GetIdentityType(),
"allowed", rsp.Allowed,
)
return apierrors.NewForbidden(gr, req.Name, fmt.Errorf("permission denied"))
}
logger.Debug("access allowed",
"resource", req.Resource,
"verb", req.Verb,
)
return nil
}

View File

@@ -1,137 +0,0 @@
package auth
import (
"context"
"errors"
"testing"
apierrors "k8s.io/apimachinery/pkg/api/errors"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTokenAccessChecker_Check(t *testing.T) {
req := authlib.CheckRequest{
Verb: "get",
Group: "provisioning.grafana.app",
Resource: "repositories",
Name: "test-repo",
Namespace: "default",
}
tests := []struct {
name string
innerResponse authlib.CheckResponse
innerErr error
authInfo *identity.StaticRequester
expectAllow bool
}{
{
name: "allowed by checker",
innerResponse: authlib.CheckResponse{Allowed: true},
authInfo: &identity.StaticRequester{Type: authlib.TypeUser},
expectAllow: true,
},
{
name: "denied by checker",
innerResponse: authlib.CheckResponse{Allowed: false},
authInfo: &identity.StaticRequester{Type: authlib.TypeUser},
expectAllow: false,
},
{
name: "error from checker",
innerErr: errors.New("access check failed"),
authInfo: &identity.StaticRequester{Type: authlib.TypeUser},
expectAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &mockInnerAccessChecker{
response: tt.innerResponse,
err: tt.innerErr,
}
checker := NewTokenAccessChecker(mock)
// Add auth info to context
testCtx := authlib.WithAuthInfo(context.Background(), tt.authInfo)
err := checker.Check(testCtx, req, "")
if tt.expectAllow {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.True(t, apierrors.IsForbidden(err), "expected Forbidden error, got: %v", err)
}
})
}
}
func TestTokenAccessChecker_NoAuthInfo(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: true},
}
checker := NewTokenAccessChecker(mock)
err := checker.Check(context.Background(), authlib.CheckRequest{}, "")
require.Error(t, err)
assert.True(t, apierrors.IsUnauthorized(err), "expected Unauthorized error")
}
func TestTokenAccessChecker_WithFallbackRole_IsNoOp(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: false},
}
checker := NewTokenAccessChecker(mock)
checkerWithFallback := checker.WithFallbackRole(identity.RoleAdmin)
// They should be the same instance
assert.Same(t, checker, checkerWithFallback, "WithFallbackRole should return same instance")
}
func TestTokenAccessChecker_FillsNamespace(t *testing.T) {
mock := &mockInnerAccessChecker{
response: authlib.CheckResponse{Allowed: true},
}
checker := NewTokenAccessChecker(mock)
ctx := authlib.WithAuthInfo(context.Background(), &identity.StaticRequester{
Type: authlib.TypeUser,
Namespace: "org-123",
})
// Request without namespace
req := authlib.CheckRequest{
Verb: "get",
Group: "provisioning.grafana.app",
Resource: "repositories",
Name: "test-repo",
// Namespace intentionally empty
}
err := checker.Check(ctx, req, "")
require.NoError(t, err)
}
// mockInnerAccessChecker implements authlib.AccessChecker for testing.
type mockInnerAccessChecker struct {
response authlib.CheckResponse
err error
}
func (m *mockInnerAccessChecker) Check(_ context.Context, _ authlib.AuthInfo, _ authlib.CheckRequest, _ string) (authlib.CheckResponse, error) {
return m.response, m.err
}
func (m *mockInnerAccessChecker) Compile(_ context.Context, _ authlib.AuthInfo, _ authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) {
return nil, nil, nil
}

View File

@@ -7,6 +7,7 @@ package v0alpha1
// MigrateJobOptionsApplyConfiguration represents a declarative configuration of the MigrateJobOptions type for use
// with apply.
type MigrateJobOptionsApplyConfiguration struct {
History *bool `json:"history,omitempty"`
Message *string `json:"message,omitempty"`
}
@@ -16,6 +17,14 @@ func MigrateJobOptions() *MigrateJobOptionsApplyConfiguration {
return &MigrateJobOptionsApplyConfiguration{}
}
// WithHistory sets the History field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the History field is set to the value of the last call.
func (b *MigrateJobOptionsApplyConfiguration) WithHistory(value bool) *MigrateJobOptionsApplyConfiguration {
b.History = &value
return b
}
// WithMessage sets the Message field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Message field is set to the value of the last call.

View File

@@ -384,7 +384,8 @@ func TestValidateJob(t *testing.T) {
Action: provisioning.JobActionMigrate,
Repository: "test-repo",
Migrate: &provisioning.MigrateJobOptions{
Message: "Migrate from unified",
History: true,
Message: "Migrate from legacy",
},
},
},

View File

@@ -238,8 +238,6 @@ func (r *gitRepository) Read(ctx context.Context, filePath, ref string) (*reposi
// Check if the path represents a directory
if safepath.IsDir(filePath) {
// Strip trailing slash for git tree lookup to avoid empty path components
finalPath = strings.TrimSuffix(finalPath, "/")
tree, err := r.client.GetTreeByPath(ctx, commit.Tree, finalPath)
if err != nil {
if errors.Is(err, nanogit.ErrObjectNotFound) {

View File

@@ -23,13 +23,7 @@ func NewSimpleRepositoryTester(validator RepositoryValidator) SimpleRepositoryTe
// TestRepository validates the repository and then runs a health check
func (t *SimpleRepositoryTester) TestRepository(ctx context.Context, repo Repository) (*provisioning.TestResults, error) {
// Determine if this is a CREATE or UPDATE operation
// If the repository has been observed by the controller (ObservedGeneration > 0),
// it's an existing repository and we should treat it as UPDATE
cfg := repo.Config()
isCreate := cfg.Status.ObservedGeneration == 0
errors := t.validator.ValidateRepository(repo, isCreate)
errors := t.validator.ValidateRepository(repo)
if len(errors) > 0 {
rsp := &provisioning.TestResults{
Code: http.StatusUnprocessableEntity, // Invalid

View File

@@ -32,9 +32,7 @@ func NewValidator(minSyncInterval time.Duration, allowedTargets []provisioning.S
}
// ValidateRepository solely does configuration checks on the repository object. It does not run a health check or compare against existing repositories.
// isCreate indicates whether this is a CREATE operation (true) or UPDATE operation (false).
// When isCreate is false, allowedTargets validation is skipped to allow existing repositories to continue working.
func (v *RepositoryValidator) ValidateRepository(repo Repository, isCreate bool) field.ErrorList {
func (v *RepositoryValidator) ValidateRepository(repo Repository) field.ErrorList {
list := repo.Validate()
cfg := repo.Config()
@@ -46,7 +44,7 @@ func (v *RepositoryValidator) ValidateRepository(repo Repository, isCreate bool)
if cfg.Spec.Sync.Target == "" {
list = append(list, field.Required(field.NewPath("spec", "sync", "target"),
"The target type is required when sync is enabled"))
} else if isCreate && !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) {
} else if !slices.Contains(v.allowedTargets, cfg.Spec.Sync.Target) {
list = append(list,
field.Invalid(
field.NewPath("spec", "target"),

View File

@@ -303,8 +303,7 @@ func TestValidateRepository(t *testing.T) {
validator := NewValidator(10*time.Second, []provisioning.SyncTargetType{provisioning.SyncTargetTypeFolder}, false)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Tests validate new configurations, so always pass isCreate=true
errors := validator.ValidateRepository(tt.repository, true)
errors := validator.ValidateRepository(tt.repository)
require.Len(t, errors, tt.expectedErrs)
if tt.validateError != nil {
tt.validateError(t, errors)

View File

@@ -1653,11 +1653,6 @@ loki_basic_auth_password =
# Accepts duration formats like: 30s, 1m, 1h.
rule_query_offset = 1m
# Default data source UID to use for query execution when importing Prometheus rules.
# This default is used when the X-Grafana-Alerting-Datasource-UID header is not provided.
# If not set, the header becomes required.
default_datasource_uid =
[recording_rules]
# Enable recording rules.
enabled = true
@@ -2269,7 +2264,7 @@ fail_tests_on_console = true
# List of targets that can be controlled by a repository, separated by |.
# Instance means the whole grafana instance will be controlled by a repository.
# Folder limits it to a folder within the grafana instance.
allowed_targets = folder
allowed_targets = instance|folder
# Whether image rendering is allowed for dashboard previews.
# Requires image rendering service to be configured.

View File

@@ -1615,11 +1615,6 @@ max_annotations_to_keep =
# Accepts duration formats like: 30s, 1m, 1h.
rule_query_offset = 1m
# Default data source UID to use for query execution when importing Prometheus rules.
# This default is used when the X-Grafana-Alerting-Datasource-UID header is not provided.
# If not set, the header becomes required.
default_datasource_uid =
#################################### Recording Rules #####################
[recording_rules]
# Enable recording rules.

View File

@@ -71,12 +71,13 @@
"id": 1,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -147,12 +148,13 @@
"id": 4,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": false,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -223,12 +225,13 @@
"id": 3,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -296,15 +299,93 @@
"x": 12,
"y": 1
},
"id": 8,
"id": 5,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"segmentCount": 1,
"segmentSpacing": 0.3,
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
},
"pluginVersion": "13.0.0-pre",
"targets": [
{
"alias": "1",
"datasource": {
"type": "grafana-testdata-datasource"
},
"max": 100,
"min": 1,
"noise": 22,
"refId": "A",
"scenarioId": "random_walk",
"spread": 22,
"startValue": 1
}
],
"title": "Spotlight",
"type": "radialbar"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 16,
"y": 1
},
"id": 8,
"maxDataPoints": 20,
"options": {
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -369,18 +450,19 @@
"gridPos": {
"h": 6,
"w": 4,
"x": 16,
"y": 1
"x": 0,
"y": 7
},
"id": 22,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -445,18 +527,19 @@
"gridPos": {
"h": 6,
"w": 4,
"x": 20,
"y": 1
"x": 4,
"y": 7
},
"id": 23,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -496,7 +579,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 7
"y": 13
},
"id": 17,
"panels": [],
@@ -533,19 +616,20 @@
},
"gridPos": {
"h": 6,
"w": 4,
"w": 5,
"x": 0,
"y": 8
"y": 14
},
"id": 18,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.1,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -609,19 +693,20 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 4,
"y": 8
"w": 5,
"x": 5,
"y": 14
},
"id": 19,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.32,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -685,19 +770,20 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 8,
"y": 8
"w": 5,
"x": 10,
"y": 14
},
"id": 20,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.57,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -761,19 +847,20 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 12,
"y": 8
"w": 5,
"x": 15,
"y": 14
},
"id": 21,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidthFactor": 0.8,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": false
"gradient": false,
"rounded": true,
"spotlight": true
},
"orientation": "auto",
"reduceOptions": {
@@ -813,7 +900,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 14
"y": 20
},
"id": 24,
"panels": [],
@@ -854,19 +941,20 @@
},
"gridPos": {
"h": 6,
"w": 4,
"w": 6,
"x": 0,
"y": 15
"y": 21
},
"id": 25,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -930,19 +1018,20 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 4,
"y": 15
"w": 6,
"x": 6,
"y": 21
},
"id": 26,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1006,19 +1095,20 @@
},
"gridPos": {
"h": 6,
"w": 4,
"x": 8,
"y": 15
"w": 5,
"x": 12,
"y": 21
},
"id": 29,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1081,20 +1171,21 @@
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 12,
"y": 15
"h": 7,
"w": 6,
"x": 0,
"y": 27
},
"id": 30,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1157,20 +1248,21 @@
"overrides": []
},
"gridPos": {
"h": 6,
"w": 4,
"x": 16,
"y": 15
"h": 7,
"w": 6,
"x": 6,
"y": 27
},
"id": 28,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.72,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": false
"gradient": false,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1206,7 +1298,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 21
"y": 34
},
"id": 31,
"panels": [],
@@ -1253,17 +1345,18 @@
"h": 10,
"w": 7,
"x": 0,
"y": 22
"y": 35
},
"id": 32,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1333,17 +1426,18 @@
"h": 10,
"w": 7,
"x": 7,
"y": 22
"y": 35
},
"id": 34,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1413,17 +1507,18 @@
"h": 10,
"w": 6,
"x": 14,
"y": 22
"y": 35
},
"id": 33,
"maxDataPoints": 20,
"options": {
"barShape": "flat",
"barWidthFactor": 0.9,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -1459,7 +1554,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 32
"y": 45
},
"id": 6,
"panels": [],
@@ -1500,20 +1595,20 @@
"h": 6,
"w": 24,
"x": 0,
"y": 33
"y": 46
},
"id": 9,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"endpointMarker": "glow",
"glow": "both",
"orientation": "auto",
"reduceOptions": {
@@ -1526,7 +1621,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1581,18 +1677,19 @@
"h": 6,
"w": 24,
"x": 0,
"y": 39
"y": 52
},
"id": 11,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.4,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1606,7 +1703,8 @@
"shape": "gauge",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": true
"sparkline": true,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1633,7 +1731,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 45
"y": 58
},
"id": 12,
"panels": [],
@@ -1675,18 +1773,19 @@
"h": 7,
"w": 4,
"x": 0,
"y": 46
"y": 59
},
"id": 13,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1700,7 +1799,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1756,18 +1856,19 @@
"h": 7,
"w": 5,
"x": 4,
"y": 46
"y": 59
},
"id": 14,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.49,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1781,7 +1882,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1836,18 +1938,19 @@
"h": 7,
"w": 5,
"x": 9,
"y": 46
"y": 59
},
"id": 15,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.84,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1861,7 +1964,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1916,18 +2020,19 @@
"h": 7,
"w": 6,
"x": 14,
"y": 46
"y": 59
},
"id": 16,
"maxDataPoints": 20,
"options": {
"barShape": "rounded",
"barWidth": 12,
"barWidthFactor": 0.66,
"effects": {
"barGlow": true,
"centerGlow": true,
"gradient": true
"gradient": true,
"rounded": true,
"spotlight": true
},
"glow": "both",
"orientation": "auto",
@@ -1941,7 +2046,8 @@
"shape": "circle",
"showThresholdLabels": false,
"showThresholdMarkers": false,
"sparkline": false
"sparkline": false,
"spotlight": true
},
"pluginVersion": "13.0.0-pre",
"targets": [
@@ -1968,7 +2074,7 @@
"h": 1,
"w": 24,
"x": 0,
"y": 53
"y": 66
},
"id": 35,
"panels": [],
@@ -1999,19 +2105,20 @@
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"h": 8,
"w": 6,
"x": 0,
"y": 54
"y": 67
},
"id": 36,
"options": {
"barShape": "flat",
"barWidthFactor": 0.5,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -2064,19 +2171,20 @@
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 54
"h": 8,
"w": 6,
"x": 6,
"y": 67
},
"id": 37,
"options": {
"barShape": "flat",
"barWidthFactor": 0.5,
"effects": {
"barGlow": false,
"centerGlow": false,
"gradient": true
"gradient": true,
"rounded": false,
"spotlight": false
},
"orientation": "auto",
"reduceOptions": {
@@ -2116,6 +2224,5 @@
"timezone": "browser",
"title": "Panel tests - Gauge (new)",
"uid": "panel-tests-gauge-new",
"version": 22,
"weekStart": ""
"version": 9
}

View File

@@ -956,6 +956,8 @@
"effects": {
"barGlow": false,
"centerGlow": false,
"rounded": false,
"spotlight": false,
"gradient": false
}
}

View File

@@ -7,15 +7,7 @@
MYSQL_PASSWORD: password
ports:
- "3306:3306"
command:
- mysqld
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --innodb_monitor_enable=all
- --default-authentication-plugin=mysql_native_password
# Please keep sql-require-primary-key option enabled, to make sure we don't accidentally introduce migration
# adding new table without PK.
- --sql-require-primary-key=ON
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all, --default-authentication-plugin=mysql_native_password]
fake-mysql-data:
image: grafana/fake-data-gen

View File

@@ -242,8 +242,6 @@ Set to `true` to import recording rules in paused state.
The UID of the data source to use for alert rule queries.
If not specified in the header, Grafana uses the configured default from `unified_alerting.prometheus_conversion.default_datasource_uid`. If neither the header nor the configuration option is provided, the request fails.
#### `X-Grafana-Alerting-Target-Datasource-UID`
The UID of the target data source for recording rules. If not specified, the value from `X-Grafana-Alerting-Datasource-UID` is used.

View File

@@ -128,48 +128,35 @@ The set up process verifies the path and provides an error message if a problem
#### Synchronization limitations
{{< admonition type="caution" >}}
Full instance sync is not available in Grafana Cloud.
Full instance sync is not available in Grafana Cloud and is experimental and unsupported in Grafana OSS/Enterprise.
{{< /admonition >}}
To have access to full instance sync you must explicitly enable the option.
The following applies:
In Grafana OSS/Enterprise:
- If you try to perform a full instance sync with resources that contain alerts or panels, the connection will be blocked.
- You won't be able to create new alerts or library panels after setup is completed.
- If you opted for full instance sync and want to use alerts and library panels, you'll have to delete the provisioned repository and connect again with folder sync.
#### Set up synchronization
You can sync external resources into a new folder without affecting the rest of your instance.
Choose to either sync your entire organization resources with external storage, or to sync certain resources to a new Grafana folder (with up to 10 connections).
To set up synchronization:
- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to.
1. Select which resources you want to sync.
- Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 connections.
1. Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI.
Next, enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI.
1. Click **Synchronize** to continue.
1. You can repeat this process for up to 10 connections.
{{< admonition type="note" >}}
Optionally, you can export any unmanaged resources into the provisioned folder. See how in [Synchronize with external storage](#synchronize-with-external-storage).
{{< /admonition >}}
Click **Synchronize** to continue.
### Synchronize with external storage
In this step you proceed to synchronize the resources selected in the previous step. Optionally, you can check the **Migrate existing resources** box to migrate your unmanaged dashboards to the provisioned folder.
After this one time step, all future updates are automatically saved to the local file path and provisioned back to the instance.
Select **Begin synchronization** to start the process. After this one time step, all future updates are automatically saved to the local file path and provisioned back to the instance.
Note that during the initial synchronization, your dashboards will be temporarily unavailable. No data or configurations will be lost.
During the initial synchronization, your dashboards will be temporarily unavailable. No data or configurations will be lost.
How long the process takes depends upon the number of resources involved.
Select **Begin synchronization** to start the process.
### Choose additional settings
If you wish, you can make any files synchronized as as **Read only** so no changes can be made to the resources through Grafana.

View File

@@ -132,35 +132,17 @@ To connect your GitHub repository:
### Choose what to synchronize
You can sync external resources into a new folder without affecting the rest of your instance.
In this step, you can decide which elements to synchronize. The available options depend on the status of your Grafana instance:
- If the instance contains resources in an incompatible data format, you'll have to migrate all the data using instance sync. Folder sync won't be supported.
- If there's already another connection using folder sync, instance sync won't be offered.
To set up synchronization:
1. Select which resources you want to sync.
- Choose **Sync all resources with external storage** if you want to sync and manage your entire Grafana instance through external storage. With this option, all of your dashboards are synced to that one repository. You can only have one provisioned connection with this selection, and you won't have the option of setting up additional repositories to connect to.
- Choose **Sync external storage to new Grafana folder** to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 connections.
1. Enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI.
1. Click **Synchronize** to continue.
1. You can repeat this process for up to 10 connections.
{{< admonition type="note" >}}
Optionally, you can export any unmanaged resources into the provisioned folder. See how in [Synchronize with external storage](#synchronize-with-external-storage).
{{< /admonition >}}
#### Full instance sync
Full instance sync is not available in Grafana Cloud and is experimental and unsupported in Grafana OSS/Enterprise.
To have access to this option you must enable experimental instance sync on purpose.
### Synchronize with external storage
After this one time step, all future updates are automatically saved to the Git repository and provisioned back to the instance.
Check the **Migrate existing resources** box to migrate your unmanaged dashboards to the provisioned folder.
Next, enter a **Display name** for the repository connection. Resources stored in this connection appear under the chosen display name in the Grafana UI. Click **Synchronize** to continue.
### Choose additional settings

View File

@@ -47,7 +47,7 @@ Using Git Sync, you can:
{{< admonition type="caution" >}}
Full instance sync is not available in Grafana Cloud and is experimental and unsupported in Grafana OSS/Enterprise.
Git Sync only works with specific folders for the moment. Full-instance sync is not currently supported.
{{< /admonition >}}
@@ -84,7 +84,7 @@ Refer to [Requirements](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/obser
- You can only sync dashboards and folders. Refer to [Supported resources](#supported-resources) for more information.
- If you're using Git Sync in Grafana OSS and Grafana Enterprise, some resources might be in an incompatible data format and won't be synced.
- Full-instance sync is not available in Grafana Cloud and is experimental in Grafana OSS and Grafana Enterprise. Refer to [Choose what to synchronize](../git-sync-setup/#choose-what-to-synchronize) for more details.
- Full-instance sync is not available in Grafana Cloud and has limitations in Grafana OSS and Grafana Enterprise. Refer to [Choose what to synchronize](../git-sync-setup/#choose-what-to-synchronize) for more details.
- When migrating to full instance sync, during the synchronization process your resources will be temporarily unavailable. No one will be able to create, edit, or delete resources during this process.
- If you want to manage existing resources with Git Sync, you need to save them as JSON files and commit them to the synced repository. Open a PR to import, copy, move, or save a dashboard.
- Restoring resources from the UI is currently not possible. As an alternative, you can restore dashboards directly in your GitHub repository by raising a PR, and they will be updated in Grafana.

View File

@@ -112,12 +112,6 @@ For example, this video demonstrates the visual Prometheus query builder:
For general information about querying in Grafana, and common options and user interface elements across all query editors, refer to [Query and transform data](ref:query-transform-data).
## Build a dashboard from the data source
After you've configured a data source, you can start creating a dashboard directly from it, by clicking the **Build a dashboard** button.
For more information, refer to [Begin dashboard creation from data source configuration](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/visualizations/dashboards/build-dashboards/create-dashboard/#begin-dashboard-creation-from-connections).
## Special data sources
Grafana includes three special data sources:

View File

@@ -54,6 +54,18 @@ SCIM offers several advantages for managing users and teams in Grafana:
## Authentication and access requirements
{{< admonition type="warning" title="Critical: Aligning SAML Identifier with SCIM externalId" >}}
When using SAML for authentication alongside SCIM provisioning, a critical security measure is to ensure proper alignment between the the SCIM user's `externalId` and the SAML user identifier. The unique identifier used for SCIM provisioning (which becomes the `externalId` in Grafana, often sourced from a stable IdP attribute like Entra ID's `user.objectid`) **must also be sent as a claim in the SAML assertion from your Identity Provider.**
Furthermore, the Grafana SAML configuration must be correctly set up to identify and use this specific claim for linking the authenticated SAML user to their SCIM-provisioned user. This can be achieved by either ensuring the primary SAML login identifier by using the `assertion_attribute_external_uid` setting in Grafana to explicitly set the name of the SAML claim that contains the stable unique identifier attribute.
**Why is this important?**
A mismatch or inconsistent mapping between this SAML login identifier and the SCIM `externalId` creates a critical security vulnerability. If these two identifiers are not reliably and uniquely aligned for each individual user, Grafana may fail to correctly link an authenticated SAML session to the intended SCIM-provisioned user profile and its associated permissions. This can enable a malicious actor to impersonate another user—for instance, by crafting a SAML assertion that, due to the identifier misalignment, incorrectly grants them the access rights of the targeted user.
Grafana relies on this linkage to correctly associate the authenticated user from SAML with the provisioned user from SCIM. Failure to ensure a consistent and unique identifier across both systems can break this linkage, leading to incorrect user mapping and potential unauthorized access.
Always verify that your SAML identity provider is configured to send a stable, unique user identifier that your SCIM configuration maps to `externalId`. Refer to your identity provider's documentation and the specific Grafana SCIM integration guides (e.g., for [Entra ID](configure-scim-with-azuread/) or [Okta](configure-scim-with-okta/)) for detailed instructions on configuring these attributes correctly.
{{< /admonition >}}
When you enable SCIM in Grafana, the following requirements and restrictions apply:
1. **Use the same identity provider for user provisioning and for authentication flow**: You must use the same identity provider for both authentication and user provisioning.
@@ -62,12 +74,6 @@ When you enable SCIM in Grafana, the following requirements and restrictions app
- Configure `userUID` SAML assertion in [Entra ID](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/saml/configure-saml-with-azuread/#configure-saml-assertions-when-using-scim-provisioning)
- Configure `userUID` SAML assertion in [Okta](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-access/configure-authentication/saml/configure-saml-with-okta/#configure-saml-assertions-when-using-scim-provisioning)
### Align SAML identifier with SCIM `externalId`
When you use SAML with SCIM provisioning, align the SCIM `externalId` with the SAML user identifier. Use a stable IdP attribute (for example, Entra ID `user.objectid`) as the SCIM `externalId`, and send that same value as a SAML claim. Configure Grafana to read this claim with the `assertion_attribute_external_uid` setting so SAML authentication links to the SCIM-provisioned user and its permissions.
If the SAML identifier and SCIM `externalId` differ, Grafana may not link the authenticated user to the intended SCIM profile, which can result in incorrect access. Verify your IdP sends a stable, unique identifier and that it matches the SCIM `externalId`. Refer to your IdP docs and the Grafana SCIM integration guides for [Entra ID](configure-scim-with-azuread/) and [Okta](configure-scim-with-okta/) for attribute configuration details.
## Configure SCIM using the Grafana user interface
You can configure SCIM in Grafana using the Grafana user interface. To do this, navigate to **Administration > Authentication > SCIM**.

View File

@@ -2052,10 +2052,6 @@ This section applies only to rules imported as Grafana-managed rules. For more i
Set the query offset to imported Grafana-managed rules when `query_offset` is not defined in the original rule group configuration. The default value is `1m`.
#### `default_datasource_uid`
Set the default data source UID to use for query execution when importing Prometheus rules. Grafana uses this default when the `X-Grafana-Alerting-Datasource-UID` header isn't provided during import. If this option isn't set, the header becomes required. The default value is empty.
<hr>
### `[annotations]`

View File

@@ -31,6 +31,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes |
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes |
| `unifiedRequestLog` | Writes error logs to the request logger | Yes |
| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes |
| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | Yes |
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes |

View File

@@ -46,7 +46,7 @@ Complete the following steps to install Grafana from the APT repository:
1. Install the prerequisite packages:
```bash
sudo apt-get install -y apt-transport-https wget
sudo apt-get install -y apt-transport-https software-properties-common wget
```
1. Import the GPG key:

View File

@@ -163,9 +163,9 @@ To add a new annotation query to a dashboard, follow these steps:
1. To create a query, do one of the following:
- Write or construct a query in the query language of your data source. The annotation query options are different for each data source. For information about annotations in a specific data source, refer to the specific [data source](ref:data-source) topic.
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries).
- Click **Replace with saved query** to reuse a [saved query](ref:saved-queries).
1. (Optional) To [save the query](ref:save-query) for reuse, open the **Saved queries** drop-down menu and click the **Save query** option.
1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
1. (Optional) Click **Test annotation query** to ensure that the query is working properly.
1. (Optional) To add subsequent queries, click **+ Add query** or **+ Add from saved queries**, and test them as many times as needed.

View File

@@ -99,7 +99,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee
- Understand the query language of the target data source.
- Ensure that data source for which you are writing a query has been added. For more information about adding a data source, refer to [Add a data source](ref:add-a-data-source) if you need instructions.
To create a dashboard, follow these steps:
**To create a dashboard**:
{{< shared id="create-dashboard" >}}
@@ -125,9 +125,9 @@ To create a dashboard, follow these steps:
1. To create a query, do one of the following:
- Write or construct a query in the query language of your data source.
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a [saved query](ref:saved-queries).
- Click **Replace with saved query** to reuse a [saved query](ref:saved-queries).
1. (Optional) To [save the query](ref:save-query) for reuse, open the **Saved queries** drop-down menu and click the **Save query** option.
1. (Optional) To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
1. Click **Refresh** to query the data source.
1. (Optional) To add subsequent queries, click **+ Add query** or **+ Add from saved queries**, and refresh the data source as many times as needed.
@@ -171,28 +171,6 @@ To create a dashboard, follow these steps:
Now, when you want to make more changes to the saved dashboard, click **Edit** in the top-right corner.
### Begin dashboard creation from data source configuration
You can start the process of creating a dashboard directly from a data source rather than from the **Dashboards** page.
To begin building a dashboard directly from a data source, follow these steps:
1. Navigate to **Connections > Data sources**.
1. On the row of the data source for which you want to build a dashboard, click **Build a dashboard**.
The empty dashboard page opens.
1. Do one of the following:
- Click **+Add visualization** to configure all the elements of the new dashboard.
- Select one of the suggested dashboards by clicking its **Use dashboard** button. This can be helpful when you're not sure how to most effectively visualize your data.
The suggested dashboards are specific to your data source type (for example, Prometheus, Loki, or Elasticsearch). If there are more than three dashboard suggestions, you can click **View all** to see the rest of them.
![Empty dashboard with add visualization and suggested dashboard options](/media/docs/grafana/dashboards/screenshot-suggested-dashboards-v12.3.png)
{{< docs/public-preview product="Suggested dashboards" >}}
1. Complete the rest of the dashboard configuration. For more detailed steps, refer to [Create a dashboard](#create-a-dashboard), beginning at step five.
## Copy a dashboard
To copy a dashboard, follow these steps:

View File

@@ -71,9 +71,8 @@ Explore consists of a toolbar, outline, query editor, the ability to add multipl
- **Run query** - Click to run your query.
- **Query editor** - Interface where you construct the query for a specific data source. Query editor elements differ based on data source. In order to run queries across multiple data sources you need to select **Mixed** from the data source picker.
- **Saved queries**:
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace query** - Reuse a saved query.
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace with saved query** - Reuse a saved query.
- **+ Add query** - Add an additional query.
- **+ Add from saved queries** - Add an additional query by reusing a saved query.

View File

@@ -88,9 +88,8 @@ The data section contains tabs where you enter queries, transform your data, and
- **Queries**
- Select your data source. You can also set or update the data source in existing dashboards using the drop-down menu in the **Queries** tab.
- **Saved queries**:
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace query** - Reuse a saved query.
- **Save query** - To [save the query](ref:save-query) for reuse, click the **Save query** button (or icon).
- **Replace with saved query** - Reuse a saved query.
- **+ Add query** - Add an additional query.
- **+ Add from saved queries** - Add an additional query by reusing a saved query.

View File

@@ -156,11 +156,11 @@ In the **Saved queries** drawer, you can:
- Edit a query title, description, tags, or the availability of the query to other users in your organization. By default, saved queries are locked for editing.
- When you access the **Saved queries** drawer from Explore, you can use the **Edit in Explore** option to edit the body of a query.
To access your saved queries, click **+ Add from saved queries** or open the **Saved queries** drop-down menu and click **Replace query** in the query editor:
To access your saved queries, click **+ Add from saved queries** or **Replace with saved query** in the query editor:
{{< figure src="/media/docs/grafana/dashboards/screenshot-use-saved-queries-v12.3.png" max-width="750px" alt="Access saved queries" >}}
Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace query** in the **Saved queries** drop-down menu updates your existing query.
Clicking **+ Add from saved queries** adds an additional query, while clicking **Replace with saved query** updates your existing query.
{{< admonition type="note" >}}
Users with Admin and Editor roles can create and save queries for reuse.
@@ -172,7 +172,7 @@ Viewers can only reuse queries.
To save a query you've created:
1. From the query editor, open the **Saved queries** drop-down menu and click the **Save query** option:
1. From the query editor, click the **Save query** icon:
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-save-query-v12.2.png" max-width="750px" alt="Save a query" >}}
@@ -227,7 +227,7 @@ To add a query, follow these steps:
1. To create a query, do one of the following:
- Write or construct a query in the query language of your data source.
- Open the **Saved queries** drop-down menu and click **Replace query** to reuse a saved query.
- Click **Replace with saved query** to reuse a saved query.
{{< admonition type="note" >}}
[Saved queries](#saved-queries) is currently in [public preview](https://grafana.com/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available.
@@ -235,7 +235,7 @@ To add a query, follow these steps:
This feature is only available on Grafana Enterprise and Grafana Cloud.
{{< /admonition >}}
1. (Optional) To [save the query](#save-a-query) for reuse, click the **Save query** option in the **Saved queries** drop-down menu.
1. (Optional) To [save the query](#save-a-query) for reuse, click the **Save query** button (or icon).
1. (Optional) Click **+ Add query** or **Add from saved queries** to add more queries as needed.
1. Click **Run queries**.

View File

@@ -1,271 +0,0 @@
import { Page } from '@playwright/test';
import { test, expect } from '@grafana/plugin-e2e';
/**
* UI selectors for Saved Searches e2e tests.
* Each selector is a function that takes the page and returns a locator.
*/
const ui = {
// Main elements
savedSearchesButton: (page: Page) => page.getByRole('button', { name: /saved searches/i }),
dropdown: (page: Page) => page.getByRole('dialog', { name: /saved searches/i }),
searchInput: (page: Page) => page.getByTestId('search-query-input'),
// Save functionality
saveButton: (page: Page) => page.getByRole('button', { name: /save current search/i }),
saveConfirmButton: (page: Page) => page.getByRole('button', { name: /^save$/i }),
saveNameInput: (page: Page) => page.getByPlaceholder(/enter a name/i),
// Action menu
actionsButton: (page: Page) => page.getByRole('button', { name: /actions/i }),
renameMenuItem: (page: Page) => page.getByText(/rename/i),
deleteMenuItem: (page: Page) => page.getByText(/^delete$/i),
setAsDefaultMenuItem: (page: Page) => page.getByText(/set as default/i),
deleteConfirmButton: (page: Page) => page.getByRole('button', { name: /^delete$/i }),
// Indicators
emptyState: (page: Page) => page.getByText(/no saved searches/i),
defaultIcon: (page: Page) => page.locator('[title="Default search"]'),
duplicateError: (page: Page) => page.getByText(/already exists/i),
};
/**
* Helper to clear saved searches storage.
* UserStorage uses localStorage as fallback, so we clear both potential keys.
*/
async function clearSavedSearches(page: Page) {
await page.evaluate(() => {
// Clear localStorage keys that might contain saved searches
// UserStorage stores under 'grafana.userstorage.alerting' pattern
const keysToRemove = Object.keys(localStorage).filter(
(key) => key.includes('alerting') && (key.includes('savedSearches') || key.includes('userstorage'))
);
keysToRemove.forEach((key) => localStorage.removeItem(key));
// Also clear session storage visited flag
const sessionKeysToRemove = Object.keys(sessionStorage).filter((key) => key.includes('alerting'));
sessionKeysToRemove.forEach((key) => sessionStorage.removeItem(key));
});
}
test.describe(
'Alert Rules - Saved Searches',
{
tag: ['@alerting'],
},
() => {
test.beforeEach(async ({ page }) => {
// Clear any saved searches from previous tests before navigating
await page.goto('/alerting/list');
await clearSavedSearches(page);
await page.reload();
});
test.afterEach(async ({ page }) => {
// Clean up saved searches after each test
await clearSavedSearches(page);
});
test('should display Saved searches button', async ({ page }) => {
await expect(ui.savedSearchesButton(page)).toBeVisible();
});
test('should open dropdown when clicking Saved searches button', async ({ page }) => {
await ui.savedSearchesButton(page).click();
await expect(ui.dropdown(page)).toBeVisible();
});
test('should show empty state when no saved searches exist', async ({ page }) => {
// Storage is cleared in beforeEach, so we should see empty state
await ui.savedSearchesButton(page).click();
await expect(ui.emptyState(page)).toBeVisible();
});
test('should enable Save current search button when search query is entered', async ({ page }) => {
// Enter a search query
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
// Open saved searches
await ui.savedSearchesButton(page).click();
await expect(ui.saveButton(page)).toBeEnabled();
});
test('should disable Save current search button when search query is empty', async ({ page }) => {
await ui.savedSearchesButton(page).click();
await expect(ui.saveButton(page)).toBeDisabled();
});
test('should save a new search', async ({ page }) => {
// Enter a search query
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
// Open saved searches
await ui.savedSearchesButton(page).click();
// Click save button
await ui.saveButton(page).click();
// Enter name and save
await ui.saveNameInput(page).fill('My Firing Rules');
await ui.saveConfirmButton(page).click();
// Verify the saved search appears in the list
await expect(page.getByText('My Firing Rules')).toBeVisible();
});
test('should show validation error for duplicate name', async ({ page }) => {
// First save a search
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
await ui.savedSearchesButton(page).click();
await ui.saveButton(page).click();
await ui.saveNameInput(page).fill('Duplicate Test');
await ui.saveConfirmButton(page).click();
// Try to save another with the same name
await ui.saveButton(page).click();
await ui.saveNameInput(page).fill('Duplicate Test');
await ui.saveConfirmButton(page).click();
// Verify validation error
await expect(ui.duplicateError(page)).toBeVisible();
});
test('should apply a saved search', async ({ page }) => {
// Create a saved search first
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
await ui.savedSearchesButton(page).click();
await ui.saveButton(page).click();
await ui.saveNameInput(page).fill('Apply Test');
await ui.saveConfirmButton(page).click();
// Clear the search
await ui.searchInput(page).clear();
await ui.searchInput(page).press('Enter');
// Apply the saved search
await ui.savedSearchesButton(page).click();
await page.getByRole('button', { name: /apply search.*apply test/i }).click();
// Verify the search input is updated
await expect(ui.searchInput(page)).toHaveValue('state:firing');
});
test('should rename a saved search', async ({ page }) => {
// Create a saved search
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
await ui.savedSearchesButton(page).click();
await ui.saveButton(page).click();
await ui.saveNameInput(page).fill('Original Name');
await ui.saveConfirmButton(page).click();
// Open action menu and click rename
await ui.actionsButton(page).click();
await ui.renameMenuItem(page).click();
// Enter new name
const renameInput = page.getByDisplayValue('Original Name');
await renameInput.clear();
await renameInput.fill('Renamed Search');
await page.keyboard.press('Enter');
// Verify the name was updated
await expect(page.getByText('Renamed Search')).toBeVisible();
await expect(page.getByText('Original Name')).not.toBeVisible();
});
test('should delete a saved search', async ({ page }) => {
// Create a saved search
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
await ui.savedSearchesButton(page).click();
await ui.saveButton(page).click();
await ui.saveNameInput(page).fill('To Delete');
await ui.saveConfirmButton(page).click();
// Verify it was saved
await expect(page.getByText('To Delete')).toBeVisible();
// Open action menu and click delete
await ui.actionsButton(page).click();
await ui.deleteMenuItem(page).click();
// Confirm delete
await ui.deleteConfirmButton(page).click();
// Verify it was deleted
await expect(page.getByText('To Delete')).not.toBeVisible();
});
test('should set a search as default', async ({ page }) => {
// Create a saved search
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
await ui.savedSearchesButton(page).click();
await ui.saveButton(page).click();
await ui.saveNameInput(page).fill('Default Test');
await ui.saveConfirmButton(page).click();
// Set as default
await ui.actionsButton(page).click();
await ui.setAsDefaultMenuItem(page).click();
// Verify the star icon appears (indicating default)
await expect(ui.defaultIcon(page)).toBeVisible();
});
test('should close dropdown when pressing Escape', async ({ page }) => {
await ui.savedSearchesButton(page).click();
await expect(ui.dropdown(page)).toBeVisible();
await page.keyboard.press('Escape');
await expect(ui.dropdown(page)).not.toBeVisible();
});
test('should cancel save mode when pressing Escape', async ({ page }) => {
// Enter a search query
await ui.searchInput(page).fill('state:firing');
await ui.searchInput(page).press('Enter');
await ui.savedSearchesButton(page).click();
// Start save mode
await ui.saveButton(page).click();
await expect(ui.saveNameInput(page)).toBeVisible();
// Press Escape to cancel
await page.keyboard.press('Escape');
// Verify we're back to list mode
await expect(ui.saveNameInput(page)).not.toBeVisible();
await expect(ui.saveButton(page)).toBeVisible();
});
}
);

Some files were not shown because too many files have changed in this diff Show More