diff --git a/.betterer.results b/.betterer.results index dcb22c40562..976a2137c61 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1029,9 +1029,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], - "public/app/features/alerting/unified/components/RuleLocation.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -1088,8 +1085,7 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/expressions/Expression.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/alerting/unified/components/mute-timings/MuteTimingTimeInterval.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -1245,9 +1241,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rule-editor/RecordingRuleEditor.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/alerting/unified/components/rule-editor/RuleEditorSection.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/alerting/unified/components/rule-editor/RuleInspector.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -1264,10 +1257,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], - "public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -1333,19 +1322,11 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], - "public/app/features/alerting/unified/components/rules/RuleListStateSection.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/alerting/unified/components/rules/RuleState.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/alerting/unified/components/rules/RuleStats.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/alerting/unified/components/rules/RulesGroup.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/alerting/unified/components/rules/state-history/LogRecordViewer.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] @@ -1356,10 +1337,6 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/alerting/unified/components/settings/VersionManager.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "public/app/features/alerting/unified/components/silences/SilencedAlertsTableRow.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -1519,9 +1496,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -1530,6 +1504,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], + "public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -1539,9 +1516,6 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/dashboard-scene/panel-edit/PanelDataPane/TransformationsDrawer.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -1566,6 +1540,27 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/scene/export/exporters.test.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + ], + "public/app/features/dashboard-scene/scene/export/exporters.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Unexpected any. Specify a different type.", "9"], + [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + ], "public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -1640,6 +1635,27 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/utils/PanelModelCompatibilityWrapper.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + ], + "public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"], + [0, 0, 0, "Do not use any type assertions.", "4"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Do not use any type assertions.", "8"], + [0, 0, 0, "Do not use any type assertions.", "9"], + [0, 0, 0, "Unexpected any. Specify a different type.", "10"] + ], "public/app/features/dashboard-scene/v2schema/test-helpers.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], @@ -1721,8 +1737,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/dashboard/components/DashboardRow/index.ts:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./DashboardRow\`)", "0"] @@ -1744,8 +1759,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] ], "public/app/features/dashboard/components/Inspector/PanelInspector.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -1841,8 +1855,7 @@ exports[`better eslint`] = { ], "public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/features/dashboard/components/VersionHistory/RevertDashboardModal.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] @@ -2087,15 +2100,13 @@ exports[`better eslint`] = { ], "public/app/features/dimensions/editors/ThresholdsEditor/ThresholdsEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] ], "public/app/features/dimensions/editors/ValueMappingsEditor/ValueMappingEditRow.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/dimensions/editors/ValueMappingsEditor/ValueMappingsEditor.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], "public/app/features/dimensions/editors/index.ts:5381": [ [0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"], @@ -2128,8 +2139,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "4"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], "public/app/features/explore/CorrelationTransformationAddModal.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -2148,9 +2158,6 @@ exports[`better eslint`] = { "public/app/features/explore/Logs/LiveLogs.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/Logs/Logs.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/explore/Logs/LogsFeedback.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -2164,15 +2171,9 @@ exports[`better eslint`] = { "public/app/features/explore/Logs/LogsTableMultiSelect.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/Logs/LogsTableNavField.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/explore/Logs/LogsVolumePanelList.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/MetaInfoText.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/explore/NodeGraph/NodeGraphContainer.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] @@ -2182,24 +2183,12 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], - "public/app/features/explore/PrometheusListView/RawListItem.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], - "public/app/features/explore/PrometheusListView/RawListItemAttributes.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] - ], "public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx:5381": [ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"], [0, 0, 0, "Do not use the t() function outside of a component or function", "1"], [0, 0, 0, "Do not use the t() function outside of a component or function", "2"], [0, 0, 0, "Do not use the t() function outside of a component or function", "3"] ], - "public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/explore/ShortLinkButtonMenu.tsx:5381": [ [0, 0, 0, "Do not use the t() function outside of a component or function", "0"] ], @@ -2227,8 +2216,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], "public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/TraceView/components/TracePageHeader/index.tsx:5381": [ [0, 0, 0, "Do not re-export imported variable (\`./TracePageHeader\`)", "0"] @@ -2236,19 +2224,10 @@ exports[`better eslint`] = { "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBar.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx:5381": [ + "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] - ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], - "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/TimelineHeaderRow/TimelineHeaderRow.tsx:5381": [ @@ -2372,10 +2351,6 @@ exports[`better eslint`] = { "public/app/features/expressions/guards.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/geo/editor/GazetteerPathEditor.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "public/app/features/geo/editor/locationModeEditor.tsx:5381": [ [0, 0, 0, "\'HorizontalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], @@ -2421,10 +2396,9 @@ exports[`better eslint`] = { "public/app/features/inspector/QueryInspector.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"], - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "3"], + [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"] ], "public/app/features/invites/SignupInvited.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -2697,9 +2671,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "8"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] ], "public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -2801,22 +2773,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"] ], - "public/app/features/trails/Breakdown/types.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/features/trails/Breakdown/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "public/app/features/trails/DataTrailBookmarks.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "public/app/features/trails/DataTrailCard.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], - "public/app/features/trails/MetricSelect/MetricSelectScene.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/.citools/bra/go.mod b/.citools/bra/go.mod index d6db529c390..6a67dbaddee 100644 --- a/.citools/bra/go.mod +++ b/.citools/bra/go.mod @@ -17,6 +17,6 @@ require ( github.com/unknwon/com v1.0.1 // indirect github.com/unknwon/log v0.0.0-20200308114134-929b1006e34a // indirect github.com/urfave/cli v1.22.16 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.32.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect ) diff --git a/.citools/bra/go.sum b/.citools/bra/go.sum index bb8abf95f22..e9a6788ad88 100644 --- a/.citools/bra/go.sum +++ b/.citools/bra/go.sum @@ -56,8 +56,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/cog/go.mod b/.citools/cog/go.mod index 364cd4e5fa8..99587abf4c9 100644 --- a/.citools/cog/go.mod +++ b/.citools/cog/go.mod @@ -41,10 +41,10 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect github.com/yalue/merged_fs v1.3.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/.citools/cog/go.sum b/.citools/cog/go.sum index ac07c9f4151..94b74898809 100644 --- a/.citools/cog/go.sum +++ b/.citools/cog/go.sum @@ -87,18 +87,18 @@ github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/.citools/cue/go.mod b/.citools/cue/go.mod index d9fd0014fb4..8cc38afcfed 100644 --- a/.citools/cue/go.mod +++ b/.citools/cue/go.mod @@ -26,12 +26,12 @@ require ( github.com/stretchr/testify v1.10.0 // indirect github.com/tetratelabs/wazero v1.6.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/.citools/cue/go.sum b/.citools/cue/go.sum index b5767f19c7b..6ee8fc180e6 100644 --- a/.citools/cue/go.sum +++ b/.citools/cue/go.sum @@ -55,18 +55,18 @@ github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzl github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/.citools/golangci-lint/go.mod b/.citools/golangci-lint/go.mod index d1e55438905..8b1e2dddb27 100644 --- a/.citools/golangci-lint/go.mod +++ b/.citools/golangci-lint/go.mod @@ -133,7 +133,7 @@ require ( github.com/polyfloyd/go-errorlint v1.7.1 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/quasilyte/go-ruleguard v0.4.4 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect @@ -192,10 +192,10 @@ require ( golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/.citools/golangci-lint/go.sum b/.citools/golangci-lint/go.sum index 981df5bab44..faf1ae88539 100644 --- a/.citools/golangci-lint/go.sum +++ b/.citools/golangci-lint/go.sum @@ -316,8 +316,8 @@ github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGC github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quasilyte/go-ruleguard v0.4.4 h1:53DncefIeLX3qEpjzlS1lyUmQoUEeOWPFWqaTJq9eAQ= @@ -497,8 +497,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -508,8 +508,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -531,8 +531,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -551,8 +551,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= @@ -571,8 +571,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/.citools/jb/go.mod b/.citools/jb/go.mod index 630753b309b..71037e9e239 100644 --- a/.citools/jb/go.mod +++ b/.citools/jb/go.mod @@ -15,6 +15,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/stretchr/testify v1.10.0 // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.32.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect ) diff --git a/.citools/jb/go.sum b/.citools/jb/go.sum index 8a9d6822ae2..cc459f28ba4 100644 --- a/.citools/jb/go.sum +++ b/.citools/jb/go.sum @@ -54,8 +54,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/lefthook/go.mod b/.citools/lefthook/go.mod index cdbdfcf9019..a72a562d2a4 100644 --- a/.citools/lefthook/go.mod +++ b/.citools/lefthook/go.mod @@ -44,10 +44,10 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/.citools/lefthook/go.sum b/.citools/lefthook/go.sum index c32157d5954..02b0b3f039a 100644 --- a/.citools/lefthook/go.sum +++ b/.citools/lefthook/go.sum @@ -96,14 +96,14 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 h1:8ajkpB4hXVftY5ko905id+dOnmorcS2CHNxxHLLDcFM= gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61/go.mod h1:IfMagxm39Ys4ybJrDb7W3Ob8RwxftP0Yy+or/NVz1O8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/.citools/swagger/go.mod b/.citools/swagger/go.mod index 7abe169767a..5dd0a6e61a8 100644 --- a/.citools/swagger/go.mod +++ b/.citools/swagger/go.mod @@ -54,13 +54,13 @@ require ( github.com/toqueteos/webbrowser v1.2.0 // indirect go.mongodb.org/mongo-driver v1.16.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/.citools/swagger/go.sum b/.citools/swagger/go.sum index 8adfd621c58..5d6f6a6eb63 100644 --- a/.citools/swagger/go.sum +++ b/.citools/swagger/go.sum @@ -106,21 +106,21 @@ go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4 go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b8a8840994b..050e18e7895 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -823,8 +823,8 @@ embed.go @grafana/grafana-as-code /.github/workflows/pr-frontend-unit-tests.yml @grafana/grafana-frontend-platform /.github/workflows/frontend-lint.yml @grafana/grafana-frontend-platform /.github/workflows/analytics-events-report.yml @grafana/grafana-frontend-platform -/.github/workflows/e2e-suite-various.yml @grafana/grafana-developer-enablement-squad /.github/workflows/pr-e2e-tests.yml @grafana/grafana-developer-enablement-squad +/.github/workflows/run-e2e-suite.yml @grafana/grafana-developer-enablement-squad # Generated files not requiring owner approval /packages/grafana-data/src/types/featureToggles.gen.ts @grafanabot diff --git a/.github/commands.json b/.github/commands.json index e6c2036d7cf..f92e29a215b 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -573,7 +573,7 @@ }, { "type": "label", - "name": "area/exploremetrics", + "name": "area/metricsdrilldown", "action": "addToProject", "addToProject": { "url": "https://github.com/orgs/grafana/projects/516" diff --git a/.github/workflows/auto-triager/labels.txt b/.github/workflows/auto-triager/labels.txt index 1a33cf676dc..517a6ad867d 100644 --- a/.github/workflows/auto-triager/labels.txt +++ b/.github/workflows/auto-triager/labels.txt @@ -33,7 +33,6 @@ area/dashboard/variable area/dashboards/panel area/data/export area/explore -area/exploremetrics area/expressions area/field/overrides area/frontend/library-panels @@ -42,6 +41,7 @@ area/image-rendering area/internationalization area/legend area/library-panel +area/metricsdrilldown area/navigation area/panel/annotation-list area/panel/barchart diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index c18f156c39a..cb57210abb0 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -39,10 +39,33 @@ jobs: retention-days: 1 name: ${{ steps.artifact.outputs.artifact }} path: grafana.tar.gz - misc-suite: + e2e-matrix: + name: ${{ matrix.suite }} + strategy: + matrix: + suite: + - various-suite + - dashboards-suite + - smoke-tests-suite + - panels-suite needs: - build-grafana - uses: ./.github/workflows/e2e-suite-various.yml - name: Various Suite + uses: ./.github/workflows/run-e2e-suite.yml with: package: ${{ needs.build-grafana.outputs.artifact }} + suite: ${{ matrix.suite }} + e2e-matrix-old-arch: + name: ${{ matrix.suite }} (old arch) + strategy: + matrix: + suite: + - old-arch/various-suite + - old-arch/dashboards-suite + - old-arch/smoke-tests-suite + - old-arch/panels-suite + needs: + - build-grafana + uses: ./.github/workflows/run-e2e-suite.yml + with: + package: ${{ needs.build-grafana.outputs.artifact }} + suite: ${{ matrix.suite }} diff --git a/.github/workflows/e2e-suite-various.yml b/.github/workflows/run-e2e-suite.yml similarity index 61% rename from .github/workflows/e2e-suite-various.yml rename to .github/workflows/run-e2e-suite.yml index ff7a69cfe48..8c3c5851325 100644 --- a/.github/workflows/e2e-suite-various.yml +++ b/.github/workflows/run-e2e-suite.yml @@ -1,4 +1,4 @@ -name: suites / various +name: e2e suite on: workflow_call: @@ -6,6 +6,9 @@ on: package: type: string required: true + suite: + type: string + required: true jobs: main: @@ -16,12 +19,14 @@ jobs: with: name: ${{ inputs.package }} - uses: dagger/dagger-for-github@8.0.0 + if: inputs.old-arch == false with: verb: run - args: go run ./pkg/build/e2e --package=grafana.tar.gz --suite=various-suite + args: go run ./pkg/build/e2e --package=grafana.tar.gz --suite=${{ inputs.suite }} + - run: echo "suite=$(echo ${{ inputs.suite }} | sed 's/\//-/g')" >> $GITHUB_ENV - uses: actions/upload-artifact@v4 - if: always() + if: ${{ always() && inputs.old-arch != true }} with: - name: e2e-various-${{github.run_number}} + name: e2e-${{ env.suite }}-${{github.run_number}} path: videos retention-days: 1 diff --git a/Makefile b/Makefile index dd40b492aeb..db63469af41 100644 --- a/Makefile +++ b/Makefile @@ -148,6 +148,13 @@ gen-cue: ## Do all CUE/Thema code generation @echo "generate code from .cue files" go generate ./kinds/gen.go go generate ./public/app/plugins/gen.go + @echo "// This file is managed by Grafana - DO NOT EDIT MANUALLY" > apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue + @echo "// Source: kinds/dashboard/dashboard_kind.cue" >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue + @echo "// To sync changes, run: make gen-cue" >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue + @echo "" >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue + @cat kinds/dashboard/dashboard_kind.cue >> apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue + @cp apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue + .PHONY: gen-cuev2 gen-cuev2: ## Do all CUE code generation diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index c6955f7be40..69419b79ff3 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -52,7 +52,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect @@ -61,7 +61,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect @@ -72,17 +72,17 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 016c1a90c58..e6c947981cb 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -126,8 +126,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= @@ -174,8 +174,8 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -203,8 +203,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -217,11 +217,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -231,23 +231,23 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -261,8 +261,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/dashboard/Makefile b/apps/dashboard/Makefile index 63f6212fd13..3a05e034c79 100644 --- a/apps/dashboard/Makefile +++ b/apps/dashboard/Makefile @@ -53,3 +53,10 @@ post-generate-cleanup: ## Clean up the generated code @sed -e '/\/\/ DeepCopyInto deep copies Spec into another Spec object/,+3d' ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp > ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp2 @rm ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp @mv ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go.tmp2 ./pkg/apis/dashboard/v1alpha1/dashboard_object_gen.go + + # Copy dashboard/v2alpha1 spec so we can use it for schema validation + @echo "// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY" > ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue + @echo "// Source: apps/dashboard/kinds/v2alpha1/dashboard_spec.cue" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue + @echo "// To sync changes, run: make generate in apps/dashboard" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue + @echo "" >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue + @cat ./kinds/v2alpha1/dashboard_spec.cue >> ./pkg/apis/dashboard/v2alpha1/dashboard_spec.cue diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index 39a2480a4c9..61bb0783df3 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -3,8 +3,9 @@ module github.com/grafana/grafana/apps/dashboard go 1.24.2 require ( + cuelang.org/go v0.11.1 github.com/grafana/grafana-app-sdk v0.35.1 - github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 + github.com/grafana/grafana-plugin-sdk-go v0.275.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.32.3 @@ -19,6 +20,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elazarl/goproxy v1.7.2 // indirect @@ -55,6 +57,7 @@ require ( github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect @@ -69,12 +72,13 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/oklog/run v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -87,9 +91,9 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect @@ -100,18 +104,18 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 04c94943cc1..1c3c8c42047 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -1,3 +1,7 @@ +cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 h1:R5wwEcbEZSBmeyg91MJZTxfd7WpBo2jPof3AYjRbxwY= +cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= +cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= +cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= @@ -20,6 +24,8 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU= github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= @@ -32,6 +38,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= +github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -56,6 +64,8 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -88,8 +98,8 @@ github.com/grafana/grafana-app-sdk v0.35.1 h1:zEXubzsQrxGBOzXJJMBwhEClC/tvPi0sfK github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI= github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM= github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= +github.com/grafana/grafana-plugin-sdk-go v0.275.0 h1:icGmZG91lVqIo79w/pSki6N44d3IjOjTfsfQPfu4THU= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 h1:/0MLOGx9Ow7ihR4smlUYHFvomXBpdpf/jLWHKNfEUiI= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -133,6 +143,8 @@ 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -154,6 +166,8 @@ github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpsp github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -173,6 +187,12 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= @@ -186,10 +206,12 @@ github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGC github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= +github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -241,14 +263,14 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= @@ -287,15 +309,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -307,23 +329,23 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -336,8 +358,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 7ac0428493b..141e420b462 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -1,8 +1,7 @@ package v2alpha1 DashboardSpec: { - // Title of dashboard. - annotations: [...AnnotationQueryKind] + annotations: [...AnnotationQueryKind] | *[] // Configuration of dashboard cursor sync behavior. // "Off" for no shared crosshair or tooltip (default). @@ -16,12 +15,12 @@ DashboardSpec: { // Whether a dashboard is editable or not. editable?: bool | *true - elements: [ElementReference.name]: Element + elements: [ElementReference.name]: Element | *{} layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind // Links with references to other dashboards or external websites. - links: [...DashboardLink] + links: [...DashboardLink] | *[] // When set to true, the dashboard will redraw panels at an interval matching the pixel width. // This will keep data "moving left" regardless of the query refresh rate. This setting helps @@ -29,14 +28,14 @@ DashboardSpec: { liveNow?: bool // When set to true, the dashboard will load all panels in the dashboard when it's loaded. - preload: bool + preload: bool | *false // Plugins only. The version of the dashboard installed together with the plugin. // This is used to determine if the dashboard should be updated when the plugin is updated. revision?: uint16 // Tags associated with dashboard. - tags: [...string] + tags: [...string] | *[] timeSettings: TimeSettingsSpec @@ -44,7 +43,7 @@ DashboardSpec: { title: string // Configured template variables. - variables: [...VariableKind] + variables: [...VariableKind] | *[] } // Supported dashboard elements @@ -85,7 +84,7 @@ AnnotationPanelFilter: { // "Off" for no shared crosshair or tooltip (default). // "Crosshair" for shared crosshair. // "Tooltip" for shared crosshair AND shared tooltip. -DashboardCursorSync: "Off" | "Crosshair" | "Tooltip" +DashboardCursorSync: "Crosshair" | "Tooltip" | *"Off" // Links with references to other dashboards or external resources DashboardLink: { @@ -101,7 +100,7 @@ DashboardLink: { // Link URL. Only required/valid if the type is link url?: string // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards - tags: [...string] + tags: [...string] | *[] // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards asDropdown: bool | *false // If true, the link will be opened in a new tab @@ -466,17 +465,17 @@ TimeSettingsSpec: { // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". to: string | *"now" // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". - autoRefresh: string // v1: refresh + autoRefresh: string | *"" // v1: refresh // Interval options available in the refresh picker dropdown. autoRefreshIntervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // v1: timepicker.refresh_intervals // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. quickRanges?: [...TimeRangeOption] // v1: timepicker.quick_ranges , not exposed in the UI // Whether timepicker is visible or not. - hideTimepicker: bool // v1: timepicker.hidden + hideTimepicker: bool | *false // v1: timepicker.hidden // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". weekStart?: "saturday" | "monday" | "sunday" // The month that the fiscal year starts on. 0 = January, 11 = December - fiscalYearStartMonth: int + fiscalYearStartMonth: int | *0 // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. nowDelay?: string // v1: timepicker.nowDelay } diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue new file mode 100644 index 00000000000..b9fd38146b7 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/dashboard_kind.cue @@ -0,0 +1,759 @@ +// This file is managed by Grafana - DO NOT EDIT MANUALLY +// Source: kinds/dashboard/dashboard_kind.cue +// To sync changes, run: make gen-cue + +package kind + +import ( + "strings" + t "time" +) + +name: "Dashboard" +maturity: "experimental" +description: "A Grafana dashboard." + +crd: dummySchema: true + +lineage: schemas: [{ + version: [0, 0] + schema: { + spec: { + // Unique numeric identifier for the dashboard. + // `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances. + id?: int64 | null // TODO eliminate this null option + + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid?: string + + // Title of dashboard. + title?: string + + // Description of dashboard. + description?: string + + // This property should only be used in dashboards defined by plugins. It is a quick check + // to see if the version has changed since the last time. + revision?: int64 + + // ID of a dashboard imported from the https://grafana.com/grafana/dashboards/ portal + gnetId?: string + + // Tags associated with dashboard. + tags?: [...string] + + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + timezone?: string | *"browser" + + // Whether a dashboard is editable or not. + editable?: bool | *true + + // Configuration of dashboard cursor sync behavior. + // Accepted values are 0 (sync turned off), 1 (shared crosshair), 2 (shared crosshair and tooltip). + graphTooltip?: #DashboardCursorSync + + // Time range for dashboard. + // Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}. + time?: { + from: string | *"now-6h" + to: string | *"now" + } + + // Configuration of the time picker shown at the top of a dashboard. + timepicker?: #TimePickerConfig + + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth?: uint8 & <12 | *0 + + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. + // This will keep data "moving left" regardless of the query refresh rate. This setting helps + // avoid dashboards presenting stale live data + liveNow?: bool + + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + weekStart?: string + + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + refresh?: string + + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + schemaVersion: uint16 | *41 + + // Version of the dashboard, incremented each time the dashboard is updated. + version?: uint32 + + // List of dashboard panels + panels?: [...(#Panel | #RowPanel)] + + // Configured template variables + templating?: { + // List of configured template variables with their saved values along with some other metadata + list?: [...#VariableModel] + } + + // Contains the list of annotations that are associated with the dashboard. + // Annotations are used to overlay event markers and overlay event tags on graphs. + // Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API. + // See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/ + annotations?: #AnnotationContainer + + // Links with references to other dashboards or external websites. + links?: [...#DashboardLink] + + // Snapshot options. They are present only if the dashboard is a snapshot. + snapshot?: #Snapshot @grafanamaturity(NeedsExpertReview) + + // When set to true, the dashboard will load all panels in the dashboard when it's loaded. + preload?: bool + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + /////////////////////////////////////// + // Definitions (referenced above) are declared below + + // TODO: this should be a regular DataQuery that depends on the selected dashboard + // these match the properties of the "grafana" datasouce that is default in most dashboards + #AnnotationTarget: { + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + limit: int64 + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + matchAny: bool + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + tags: [...string] + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + type: string + ... // datasource will stick their raw DataQuery here + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + #AnnotationPanelFilter: { + // Should the specified panels be included or excluded + exclude?: bool | *false + + // Panel IDs that should be included or excluded + ids: [...uint8] + } @cuetsy(kind="interface") + + // Contains the list of annotations that are associated with the dashboard. + // Annotations are used to overlay event markers and overlay event tags on graphs. + // Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API. + // See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/ + #AnnotationContainer: { + // List of annotations + list?: [...#AnnotationQuery] + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // TODO docs + // FROM: AnnotationQuery in grafana-data/src/types/annotations.ts + #AnnotationQuery: { + // Name of annotation. + name: string + + // Datasource where the annotations data is + datasource: #DataSourceRef + + // When enabled the annotation query is issued with every dashboard refresh + enable: bool | *true + + // Annotation queries can be toggled on or off at the top of the dashboard. + // When hide is true, the toggle is not shown in the dashboard. + hide?: bool | *false + + // Color to use for the annotation event markers + iconColor: string + + // Filters to apply when fetching annotations + filter?: #AnnotationPanelFilter + + // TODO.. this should just be a normal query target + target?: #AnnotationTarget + + // TODO -- this should not exist here, it is based on the --grafana-- datasource + type?: string @grafanamaturity(NeedsExpertReview) + + // Set to 1 for the standard annotation query all dashboards have by default. + builtIn?: number | *0 + + // unless datasources have migrated to the target+mapping, + // they just spread their query into the base object :( + ... + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // A variable is a placeholder for a value. You can use variables in metric queries and in panel titles. + #VariableModel: { + // Type of variable + type: #VariableType + // Name of variable + name: string + // Optional display name + label?: string + // Visibility configuration for the variable + hide?: #VariableHide + // Whether the variable value should be managed by URL query params or not + skipUrlSync?: bool | *false + // Description of variable. It can be defined but `null`. + description?: string + // Query used to fetch values for a variable + query?: string | {...} + // Data source used to fetch values for a variable. It can be defined but `null`. + datasource?: #DataSourceRef + // Shows current selected variable text/value on the dashboard + current?: #VariableOption + // Whether multiple values can be selected or not from variable value list + multi?: bool | *false + // Allow custom values to be entered in the variable + allowCustomValue?: bool | *true + // Options that can be selected for a variable. + options?: [...#VariableOption] + // Options to config when to refresh a variable + refresh?: #VariableRefresh + // Options sort order + sort?: #VariableSort + // Whether all value option is available or not + includeAll?: bool | *false + // Custom all value + allValue?: string + // Optional field, if you want to extract part of a series name or metric node segment. + // Named capture groups can be used to separate the display text and value. + regex?: string + ... + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // Option to be selected in a variable. + #VariableOption: { + // Whether the option is selected or not + selected?: bool + // Text to be displayed for the option + text: string | [...string] + // Value of the option + value: string | [...string] + } @cuetsy(kind="interface") + + // Options to config when to refresh a variable + // `0`: Never refresh the variable + // `1`: Queries the data source every time the dashboard loads. + // `2`: Queries the data source when the dashboard time range changes. + #VariableRefresh: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="never|onDashboardLoad|onTimeRangeChanged") + + // Determine if the variable shows on dashboard + // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing). + #VariableHide: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable") @grafana(TSVeneer="type") + + // Sort variable options + // Accepted values are: + // `0`: No sorting + // `1`: Alphabetical ASC + // `2`: Alphabetical DESC + // `3`: Numerical ASC + // `4`: Numerical DESC + // `5`: Alphabetical Case Insensitive ASC + // `6`: Alphabetical Case Insensitive DESC + // `7`: Natural ASC + // `8`: Natural DESC + #VariableSort: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 @cuetsy(kind="enum",memberNames="disabled|alphabeticalAsc|alphabeticalDesc|numericalAsc|numericalDesc|alphabeticalCaseInsensitiveAsc|alphabeticalCaseInsensitiveDesc|naturalAsc|naturalDesc") + + // Ref to a DataSource instance + #DataSourceRef: { + // The plugin type-id + type?: string + + // Specific datasource instance + uid?: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // Links with references to other dashboards or external resources + #DashboardLink: { + // Title to display with the link + title: string + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + type: #DashboardLinkType + // Icon name to be displayed with the link + icon: string + // Tooltip to display when the user hovers their mouse over it + tooltip: string + // Link URL. Only required/valid if the type is link + url?: string + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + tags: [...string] + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + asDropdown: bool | *false + // If true, the link will be opened in a new tab + targetBlank: bool | *false + // If true, includes current template variables values in the link as query params + includeVars: bool | *false + // If true, includes current time range in the link as query params + keepTime: bool | *false + } @cuetsy(kind="interface") + + // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") + + // Dashboard variable type + // `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. + // `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). + // `constant`: Define a hidden constant. + // `datasource`: Quickly change the data source for an entire dashboard. + // `interval`: Interval variables represent time spans. + // `textbox`: Display a free text input field with an optional default value. + // `custom`: Define the variable options manually using a comma-separated list. + // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables + #VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | + "system" | "snapshot" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview) + + // Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. + // Continuous color interpolates a color using the percentage of a value relative to min and max. + // Accepted values are: + // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold + // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations + // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations + // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode + // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode + // `continuous-YlRd`: Continuous Yellow-Red palette mode + // `continuous-BlPu`: Continuous Blue-Purple palette mode + // `continuous-YlBl`: Continuous Yellow-Blue palette mode + // `continuous-blues`: Continuous Blue palette mode + // `continuous-reds`: Continuous Red palette mode + // `continuous-greens`: Continuous Green palette mode + // `continuous-purples`: Continuous Purple palette mode + // `shades`: Shades of a single color. Specify a single color, useful in an override rule. + // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + + // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. + #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") + + // Map a field to a color. + #FieldColor: { + // The main color scheme mode. + mode: #FieldColorModeId + // The fixed color value for fixed or shades color modes. + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes. + seriesBy?: #FieldColorSeriesByMode + } @cuetsy(kind="interface") + + // Position and dimensions of a panel in the grid + #GridPos: { + // Panel height. The height is the number of rows from the top edge of the panel. + h: uint32 & >0 | *9 + // Panel width. The width is the number of columns from the left edge of the panel. + w: uint32 & >0 & <=24 | *12 + // Panel x. The x coordinate is the number of columns from the left edge of the grid + x: uint32 & >=0 & <24 | *0 + // Panel y. The y coordinate is the number of rows from the top edge of the grid + y: uint32 & >=0 | *0 + // Whether the panel is fixed within the grid. If true, the panel will not be affected by other panels' interactions + static?: bool + } @cuetsy(kind="interface") + + // User-defined value for a metric that triggers visual changes in a panel when this value is met or exceeded + // They are used to conditionally style and color visualizations based on query results , and can be applied to most visualizations. + #Threshold: { + // Value represents a specified metric for the threshold, which triggers a visual change in the dashboard when this value is met or exceeded. + // Nulls currently appear here when serializing -Infinity to JSON. + value: number | null @grafanamaturity(NeedsExpertReview) + // Color represents the color of the visual change that will occur in the dashboard when the threshold value is met or exceeded. + color: string @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Thresholds can either be `absolute` (specific number) or `percentage` (relative to min or max, it will be values between 0 and 1). + #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum",memberNames="Absolute|Percentage") + + // Thresholds configuration for the panel + #ThresholdsConfig: { + // Thresholds mode. + mode: #ThresholdsMode + + // Must be sorted by 'value', first value is always -Infinity + steps: [...#Threshold] @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Allow to transform the visual representation of specific data values in a visualization, irrespective of their original units + #ValueMapping: #ValueMap | #RangeMap | #RegexMap | #SpecialValueMap @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview) + + // Supported value mapping types + // `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. + // `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. + // `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. + // `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. + #MappingType: "value" | "range" | "regex" | "special" @cuetsy(kind="enum",memberNames="ValueToText|RangeToText|RegexToText|SpecialValue") @grafanamaturity(NeedsExpertReview) + + // Maps text values to a color or different display text and color. + // For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. + #ValueMap: { + type: #MappingType & "value" + // Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } + options: [string]: #ValueMappingResult + } @cuetsy(kind="interface") + + // Maps numerical ranges to a display text and color. + // For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. + #RangeMap: { + type: #MappingType & "range" + // Range to match against and the result to apply when the value is within the range + options: { + // Min value of the range. It can be null which means -Infinity + from: float64 | null + // Max value of the range. It can be null which means +Infinity + to: float64 | null + // Config to apply when the value is within the range + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Maps regular expressions to replacement text and a color. + // For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. + #RegexMap: { + type: #MappingType & "regex" + // Regular expression to match against and the result to apply when the value matches the regex + options: { + // Regular expression to match against + pattern: string + // Config to apply when the value matches the regex + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. + // See SpecialValueMatch to see the list of special values. + // For example, you can configure a special value mapping so that null values appear as N/A. + #SpecialValueMap: { + type: #MappingType & "special" + options: { + // Special value to match against + match: #SpecialValueMatch + // Config to apply when the value matches the special value + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Special value types supported by the `SpecialValueMap` + #SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cuetsy(kind="enum",memberNames="True|False|Null|NaN|NullAndNan|Empty") + + // Result used as replacement with text and color when the value matches + #ValueMappingResult: { + // Text to display when the value matches + text?: string + // Text to use when the value matches + color?: string + // Icon to display when the value matches. Only specific visualizations. + icon?: string + // Position in the mapping array. Only used internally. + index?: int32 + } @cuetsy(kind="interface") + + // Transformations allow to manipulate data returned by a query before the system applies a visualization. + // Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, + // use the output of one transformation as the input to another transformation, etc. + #DataTransformerConfig: { + // Unique identifier of transformer + id: string + // Disabled transformations are skipped + disabled?: bool + // Optional frame matcher. When missing it will be applied to all results + filter?: #MatcherConfig + // Where to pull DataFrames from as input to transformation + topic?: "series" | "annotations" | "alertStates" // replaced with common.DataTopic + // Options to be passed to the transformer + // Valid options depend on the transformer id + options: _ + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // Counterpart for TypeScript's TimeOption type. + #TimeOption: { + display: string + from: string + to: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // Time picker configuration + // It defines the default config for the time picker and the refresh picker for the specific dashboard. + #TimePickerConfig: { + // Whether timepicker is visible or not. + hidden?: bool | *false + // Interval options available in the refresh picker dropdown. + refresh_intervals?: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + // Quick ranges for time picker. + quick_ranges?: [...#TimeOption] + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + nowDelay?: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + #DashboardCursorSync: *0 | 1 | 2 @cuetsy(kind="enum",memberNames="Off|Crosshair|Tooltip") + + // Schema for panel targets is specified by datasource + // plugins. We use a placeholder definition, which the Go + // schema loader either left open/as-is with the Base + // variant of the Dashboard and Panel families, or filled + // with types derived from plugins in the Instance variant. + // When working directly from CUE, importers can extend this + // type directly to achieve the same effect. + #Target: {...} + + // A dashboard snapshot shares an interactive dashboard publicly. + // It is a read-only version of a dashboard, and is not editable. + // It is possible to create a snapshot of a snapshot. + // Grafana strips away all sensitive information from the dashboard. + // Sensitive information stripped: queries (metric, template,annotation) and panel links. + #Snapshot: { + // Time when the snapshot was created + created: string & t.Time + // Time when the snapshot expires, default is never to expire + expires: string @grafanamaturity(NeedsExpertReview) + // Is the snapshot saved in an external grafana instance + external: bool @grafanamaturity(NeedsExpertReview) + // external url, if snapshot was shared in external grafana instance + externalUrl: string @grafanamaturity(NeedsExpertReview) + // original url, url of the dashboard that was snapshotted + originalUrl: string @grafanamaturity(NeedsExpertReview) + // Unique identifier of the snapshot + id: uint32 @grafanamaturity(NeedsExpertReview) + // Optional, defined the unique key of the snapshot, required if external is true + key: string @grafanamaturity(NeedsExpertReview) + // Optional, name of the snapshot + name: string @grafanamaturity(NeedsExpertReview) + // org id of the snapshot + orgId: uint32 @grafanamaturity(NeedsExpertReview) + // last time when the snapshot was updated + updated: string & t.Time + // url of the snapshot, if snapshot was shared internally + url?: string @grafanamaturity(NeedsExpertReview) + // user id of the snapshot creator + userId: uint32 @grafanamaturity(NeedsExpertReview) + } @grafanamaturity(NeedsExpertReview) + + // Dashboard panels are the basic visualization building blocks. + #Panel: { + // The panel plugin type id. This is used to find the plugin to display the panel. + type: string & strings.MinRunes(1) + + // Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. + id?: uint32 + + // The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. + pluginVersion?: string + + // Depends on the panel plugin. See the plugin documentation for details. + targets?: [...#Target] + + // Panel title. + title?: string + + // Panel description. + description?: string + + // Whether to display the panel without a background. + transparent?: bool | *false + + // The datasource used in all targets. + datasource?: #DataSourceRef + + // Grid position. + gridPos?: #GridPos + + // Panel links. + links?: [...#DashboardLink] + + // Name of template variable to repeat for. + repeat?: string + + // Direction to repeat in if 'repeat' is set. + // `h` for horizontal, `v` for vertical. + repeatDirection?: *"h" | "v" + + // Option for repeated panels that controls max items per row + // Only relevant for horizontally repeated panels + maxPerRow?: number + + // The maximum number of data points that the panel queries are retrieving. + maxDataPoints?: number + + // List of transformations that are applied to the panel data before rendering. + // When there are multiple transformations, Grafana applies them in the order they are listed. + // Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. + transformations?: [...#DataTransformerConfig] + + // The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables. + // This value must be formatted as a number followed by a valid time + // identifier like: "40s", "3d", etc. + // See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options + interval?: string + + // Overrides the relative time range for individual panels, + // which causes them to be different than what is selected in + // the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different + // time periods or days on the same dashboard. + // The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far), + // `now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years). + // Note: Panel time overrides have no effect when the dashboard’s time range is absolute. + // See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options + timeFrom?: string + + // Overrides the time range for individual panels by shifting its start and end relative to the time picker. + // For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`. + // Note: Panel time overrides have no effect when the dashboard’s time range is absolute. + // See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options + timeShift?: string + + // Controls if the timeFrom or timeShift overrides are shown in the panel header + hideTimeOverride?: bool + + // Dynamically load the panel + libraryPanel?: #LibraryPanelRef + + // Sets panel queries cache timeout. + cacheTimeout?: string + + // Overrides the data source configured time-to-live for a query cache item in milliseconds + queryCachingTTL?: number + + // It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. + options?: {...} @grafanamaturity(NeedsExpertReview) + + // Field options allow you to change how the data is displayed in your visualizations. + fieldConfig?: #FieldConfigSource + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. + // Each column within this structure is called a field. A field can represent a single time series or table column. + // Field options allow you to change how the data is displayed in your visualizations. + #FieldConfigSource: { + // Defaults are the options applied to all fields. + defaults: #FieldConfig + // Overrides are the options applied to specific fields overriding the defaults. + overrides: [...{ + matcher: #MatcherConfig + properties: [...#DynamicConfigValue] + }] @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // A library panel is a reusable panel that you can use in any dashboard. + // When you make a change to a library panel, that change propagates to all instances of where the panel is used. + // Library panels streamline reuse of panels across multiple dashboards. + #LibraryPanelRef: { + // Library panel name + name: string + // Library panel uid + uid: string + } @cuetsy(kind="interface") + + // Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. + // It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. + #MatcherConfig: { + // The matcher id. This is used to find the matcher implementation from registry. + id: string | *"" @grafanamaturity(NeedsExpertReview) + // The matcher options. This is specific to the matcher implementation. + options?: _ @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + #DynamicConfigValue: { + id: string | *"" @grafanamaturity(NeedsExpertReview) + value?: _ @grafanamaturity(NeedsExpertReview) + } + + // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. + // Each column within this structure is called a field. A field can represent a single time series or table column. + // Field options allow you to change how the data is displayed in your visualizations. + #FieldConfig: { + // The display value for this field. This supports template variables blank is auto + displayName?: string @grafanamaturity(NeedsExpertReview) + + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string @grafanamaturity(NeedsExpertReview) + + // Human readable field metadata + description?: string @grafanamaturity(NeedsExpertReview) + + // An explicit path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string @grafanamaturity(NeedsExpertReview) + + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool @grafanamaturity(NeedsExpertReview) + + // True if data source field supports ad-hoc filters + filterable?: bool @grafanamaturity(NeedsExpertReview) + + // Unit a field should use. The unit you select is applied to all fields except time. + // You can use the units ID availables in Grafana or a custom unit. + // Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts + // As custom unit, you can use the following formats: + // `suffix:` for custom unit that should go after value. + // `prefix:` for custom unit that should go before value. + // `time:` For custom date time formats type for example `time:YYYY-MM-DD`. + // `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. + // `count:` for a custom count unit. + // `currency:` for custom a currency unit. + unit?: string @grafanamaturity(NeedsExpertReview) + + // Specify the number of decimals Grafana includes in the rendered value. + // If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. + // For example 1.1234 will display as 1.12 and 100.456 will display as 100. + // To display all decimals, set the unit to `String`. + decimals?: number @grafanamaturity(NeedsExpertReview) + + // The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + min?: number @grafanamaturity(NeedsExpertReview) + // The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + max?: number @grafanamaturity(NeedsExpertReview) + + // Convert input values into a display string + mappings?: [...#ValueMapping] @grafanamaturity(NeedsExpertReview) + + // Map numeric values to states + thresholds?: #ThresholdsConfig @grafanamaturity(NeedsExpertReview) + + // Panel color configuration + color?: #FieldColor + + // The behavior when clicking on a result + links?: [...] @grafanamaturity(NeedsExpertReview) + + // Alternative to empty string + noValue?: string @grafanamaturity(NeedsExpertReview) + + // custom is specified by the FieldConfig field + // in panel plugin schemas. + custom?: {...} @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // Row panel + #RowPanel: { + // The panel type + type: "row" + + // Whether this row should be collapsed or not. + collapsed: bool | *false + + // Row title + title?: string + + // Name of default datasource for the row + datasource?: #DataSourceRef + + // Row grid position + gridPos?: #GridPos + + // Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. + id: uint32 + + // List of panels in the row + panels: [...#Panel] + + // Name of template variable to repeat for. + repeat?: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + } +}, +] diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go new file mode 100644 index 00000000000..6fdf1dd4515 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go @@ -0,0 +1,90 @@ +package v0alpha1 + +import ( + _ "embed" + json "encoding/json" + fmt "fmt" + "strings" + "sync" + + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" + "k8s.io/apimachinery/pkg/util/validation/field" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/errors" + cuejson "cuelang.org/go/encoding/json" +) + +func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorList, field.ErrorList) { + var schemaVersionError field.ErrorList + schemaVersion := schemaversion.GetSchemaVersion(obj.Spec.Object) + if schemaVersion != schemaversion.LATEST_VERSION { + schemaVersionError = field.ErrorList{field.Invalid(field.NewPath("spec", "schemaVersion"), field.OmitValueType{}, fmt.Sprintf("Schema version %d is not supported - please upgrade to %d", schemaVersion, schemaversion.LATEST_VERSION))} + if !forceValidation { + return nil, schemaVersionError + } + } + + data, err := json.Marshal(obj.Spec.Object) + if err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec"), field.OmitValueType{}, err.Error()), + }, schemaVersionError + } + + if err := cuejson.Validate(data, getCueSchema()); err != nil { + errs := field.ErrorList{} + + for _, e := range errors.Errors(err) { + if + // We don't want to return confusing "empty disjunction" errors, + // because the users don't necessarily understand what to do with them. + // For empty disjunctions, CUE will also return more specific errors, + // so we can safely ignore the generic ones. + strings.Contains(e.Error(), "disjunction") || + // We don't want to return errors about unknown fields either. + strings.Contains(e.Error(), "field not allowed") { + continue + } + + // We want to manually format the error message, + // because e.Error() contains the full CUE path. + format, args := e.Msg() + + errs = append(errs, field.Invalid( + field.NewPath(formatErrorPath(e.Path())), + field.OmitValueType{}, + fmt.Sprintf(format, args...), + )) + } + + return errs, schemaVersionError + } + + return nil, schemaVersionError +} + +func formatErrorPath(path []string) string { + // omitting the "lineage.schemas[0].schema.spec" prefix here. + return strings.Join(path[4:], ".") +} + +var ( + compiledSchema cue.Value + getSchemaOnce sync.Once +) + +//go:embed dashboard_kind.cue +var schemaSource string + +func getCueSchema() cue.Value { + getSchemaOnce.Do(func() { + cueCtx := cuecontext.New() + compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + cue.ParsePath("lineage.schemas[0].schema.spec"), + ) + }) + + return compiledSchema +} diff --git a/apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue b/apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue new file mode 100644 index 00000000000..b9fd38146b7 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v1alpha1/dashboard_kind.cue @@ -0,0 +1,759 @@ +// This file is managed by Grafana - DO NOT EDIT MANUALLY +// Source: kinds/dashboard/dashboard_kind.cue +// To sync changes, run: make gen-cue + +package kind + +import ( + "strings" + t "time" +) + +name: "Dashboard" +maturity: "experimental" +description: "A Grafana dashboard." + +crd: dummySchema: true + +lineage: schemas: [{ + version: [0, 0] + schema: { + spec: { + // Unique numeric identifier for the dashboard. + // `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances. + id?: int64 | null // TODO eliminate this null option + + // Unique dashboard identifier that can be generated by anyone. string (8-40) + uid?: string + + // Title of dashboard. + title?: string + + // Description of dashboard. + description?: string + + // This property should only be used in dashboards defined by plugins. It is a quick check + // to see if the version has changed since the last time. + revision?: int64 + + // ID of a dashboard imported from the https://grafana.com/grafana/dashboards/ portal + gnetId?: string + + // Tags associated with dashboard. + tags?: [...string] + + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + timezone?: string | *"browser" + + // Whether a dashboard is editable or not. + editable?: bool | *true + + // Configuration of dashboard cursor sync behavior. + // Accepted values are 0 (sync turned off), 1 (shared crosshair), 2 (shared crosshair and tooltip). + graphTooltip?: #DashboardCursorSync + + // Time range for dashboard. + // Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}. + time?: { + from: string | *"now-6h" + to: string | *"now" + } + + // Configuration of the time picker shown at the top of a dashboard. + timepicker?: #TimePickerConfig + + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth?: uint8 & <12 | *0 + + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. + // This will keep data "moving left" regardless of the query refresh rate. This setting helps + // avoid dashboards presenting stale live data + liveNow?: bool + + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + weekStart?: string + + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + refresh?: string + + // Version of the JSON schema, incremented each time a Grafana update brings + // changes to said schema. + schemaVersion: uint16 | *41 + + // Version of the dashboard, incremented each time the dashboard is updated. + version?: uint32 + + // List of dashboard panels + panels?: [...(#Panel | #RowPanel)] + + // Configured template variables + templating?: { + // List of configured template variables with their saved values along with some other metadata + list?: [...#VariableModel] + } + + // Contains the list of annotations that are associated with the dashboard. + // Annotations are used to overlay event markers and overlay event tags on graphs. + // Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API. + // See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/ + annotations?: #AnnotationContainer + + // Links with references to other dashboards or external websites. + links?: [...#DashboardLink] + + // Snapshot options. They are present only if the dashboard is a snapshot. + snapshot?: #Snapshot @grafanamaturity(NeedsExpertReview) + + // When set to true, the dashboard will load all panels in the dashboard when it's loaded. + preload?: bool + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + /////////////////////////////////////// + // Definitions (referenced above) are declared below + + // TODO: this should be a regular DataQuery that depends on the selected dashboard + // these match the properties of the "grafana" datasouce that is default in most dashboards + #AnnotationTarget: { + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + limit: int64 + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + matchAny: bool + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + tags: [...string] + // Only required/valid for the grafana datasource... + // but code+tests is already depending on it so hard to change + type: string + ... // datasource will stick their raw DataQuery here + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + #AnnotationPanelFilter: { + // Should the specified panels be included or excluded + exclude?: bool | *false + + // Panel IDs that should be included or excluded + ids: [...uint8] + } @cuetsy(kind="interface") + + // Contains the list of annotations that are associated with the dashboard. + // Annotations are used to overlay event markers and overlay event tags on graphs. + // Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API. + // See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/ + #AnnotationContainer: { + // List of annotations + list?: [...#AnnotationQuery] + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // TODO docs + // FROM: AnnotationQuery in grafana-data/src/types/annotations.ts + #AnnotationQuery: { + // Name of annotation. + name: string + + // Datasource where the annotations data is + datasource: #DataSourceRef + + // When enabled the annotation query is issued with every dashboard refresh + enable: bool | *true + + // Annotation queries can be toggled on or off at the top of the dashboard. + // When hide is true, the toggle is not shown in the dashboard. + hide?: bool | *false + + // Color to use for the annotation event markers + iconColor: string + + // Filters to apply when fetching annotations + filter?: #AnnotationPanelFilter + + // TODO.. this should just be a normal query target + target?: #AnnotationTarget + + // TODO -- this should not exist here, it is based on the --grafana-- datasource + type?: string @grafanamaturity(NeedsExpertReview) + + // Set to 1 for the standard annotation query all dashboards have by default. + builtIn?: number | *0 + + // unless datasources have migrated to the target+mapping, + // they just spread their query into the base object :( + ... + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // A variable is a placeholder for a value. You can use variables in metric queries and in panel titles. + #VariableModel: { + // Type of variable + type: #VariableType + // Name of variable + name: string + // Optional display name + label?: string + // Visibility configuration for the variable + hide?: #VariableHide + // Whether the variable value should be managed by URL query params or not + skipUrlSync?: bool | *false + // Description of variable. It can be defined but `null`. + description?: string + // Query used to fetch values for a variable + query?: string | {...} + // Data source used to fetch values for a variable. It can be defined but `null`. + datasource?: #DataSourceRef + // Shows current selected variable text/value on the dashboard + current?: #VariableOption + // Whether multiple values can be selected or not from variable value list + multi?: bool | *false + // Allow custom values to be entered in the variable + allowCustomValue?: bool | *true + // Options that can be selected for a variable. + options?: [...#VariableOption] + // Options to config when to refresh a variable + refresh?: #VariableRefresh + // Options sort order + sort?: #VariableSort + // Whether all value option is available or not + includeAll?: bool | *false + // Custom all value + allValue?: string + // Optional field, if you want to extract part of a series name or metric node segment. + // Named capture groups can be used to separate the display text and value. + regex?: string + ... + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // Option to be selected in a variable. + #VariableOption: { + // Whether the option is selected or not + selected?: bool + // Text to be displayed for the option + text: string | [...string] + // Value of the option + value: string | [...string] + } @cuetsy(kind="interface") + + // Options to config when to refresh a variable + // `0`: Never refresh the variable + // `1`: Queries the data source every time the dashboard loads. + // `2`: Queries the data source when the dashboard time range changes. + #VariableRefresh: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="never|onDashboardLoad|onTimeRangeChanged") + + // Determine if the variable shows on dashboard + // Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing). + #VariableHide: 0 | 1 | 2 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable") @grafana(TSVeneer="type") + + // Sort variable options + // Accepted values are: + // `0`: No sorting + // `1`: Alphabetical ASC + // `2`: Alphabetical DESC + // `3`: Numerical ASC + // `4`: Numerical DESC + // `5`: Alphabetical Case Insensitive ASC + // `6`: Alphabetical Case Insensitive DESC + // `7`: Natural ASC + // `8`: Natural DESC + #VariableSort: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 @cuetsy(kind="enum",memberNames="disabled|alphabeticalAsc|alphabeticalDesc|numericalAsc|numericalDesc|alphabeticalCaseInsensitiveAsc|alphabeticalCaseInsensitiveDesc|naturalAsc|naturalDesc") + + // Ref to a DataSource instance + #DataSourceRef: { + // The plugin type-id + type?: string + + // Specific datasource instance + uid?: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // Links with references to other dashboards or external resources + #DashboardLink: { + // Title to display with the link + title: string + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + type: #DashboardLinkType + // Icon name to be displayed with the link + icon: string + // Tooltip to display when the user hovers their mouse over it + tooltip: string + // Link URL. Only required/valid if the type is link + url?: string + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + tags: [...string] + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + asDropdown: bool | *false + // If true, the link will be opened in a new tab + targetBlank: bool | *false + // If true, includes current template variables values in the link as query params + includeVars: bool | *false + // If true, includes current time range in the link as query params + keepTime: bool | *false + } @cuetsy(kind="interface") + + // Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + #DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type") + + // Dashboard variable type + // `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. + // `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). + // `constant`: Define a hidden constant. + // `datasource`: Quickly change the data source for an entire dashboard. + // `interval`: Interval variables represent time spans. + // `textbox`: Display a free text input field with an optional default value. + // `custom`: Define the variable options manually using a comma-separated list. + // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables + #VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | + "system" | "snapshot" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview) + + // Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. + // Continuous color interpolates a color using the percentage of a value relative to min and max. + // Accepted values are: + // `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold + // `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations + // `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations + // `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode + // `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode + // `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode + // `continuous-YlRd`: Continuous Yellow-Red palette mode + // `continuous-BlPu`: Continuous Blue-Purple palette mode + // `continuous-YlBl`: Continuous Yellow-Blue palette mode + // `continuous-blues`: Continuous Blue palette mode + // `continuous-reds`: Continuous Red palette mode + // `continuous-greens`: Continuous Green palette mode + // `continuous-purples`: Continuous Purple palette mode + // `shades`: Shades of a single color. Specify a single color, useful in an override rule. + // `fixed`: Fixed color mode. Specify a single color, useful in an override rule. + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview) + + // Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. + #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") + + // Map a field to a color. + #FieldColor: { + // The main color scheme mode. + mode: #FieldColorModeId + // The fixed color value for fixed or shades color modes. + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes. + seriesBy?: #FieldColorSeriesByMode + } @cuetsy(kind="interface") + + // Position and dimensions of a panel in the grid + #GridPos: { + // Panel height. The height is the number of rows from the top edge of the panel. + h: uint32 & >0 | *9 + // Panel width. The width is the number of columns from the left edge of the panel. + w: uint32 & >0 & <=24 | *12 + // Panel x. The x coordinate is the number of columns from the left edge of the grid + x: uint32 & >=0 & <24 | *0 + // Panel y. The y coordinate is the number of rows from the top edge of the grid + y: uint32 & >=0 | *0 + // Whether the panel is fixed within the grid. If true, the panel will not be affected by other panels' interactions + static?: bool + } @cuetsy(kind="interface") + + // User-defined value for a metric that triggers visual changes in a panel when this value is met or exceeded + // They are used to conditionally style and color visualizations based on query results , and can be applied to most visualizations. + #Threshold: { + // Value represents a specified metric for the threshold, which triggers a visual change in the dashboard when this value is met or exceeded. + // Nulls currently appear here when serializing -Infinity to JSON. + value: number | null @grafanamaturity(NeedsExpertReview) + // Color represents the color of the visual change that will occur in the dashboard when the threshold value is met or exceeded. + color: string @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Thresholds can either be `absolute` (specific number) or `percentage` (relative to min or max, it will be values between 0 and 1). + #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum",memberNames="Absolute|Percentage") + + // Thresholds configuration for the panel + #ThresholdsConfig: { + // Thresholds mode. + mode: #ThresholdsMode + + // Must be sorted by 'value', first value is always -Infinity + steps: [...#Threshold] @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Allow to transform the visual representation of specific data values in a visualization, irrespective of their original units + #ValueMapping: #ValueMap | #RangeMap | #RegexMap | #SpecialValueMap @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview) + + // Supported value mapping types + // `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. + // `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. + // `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. + // `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. + #MappingType: "value" | "range" | "regex" | "special" @cuetsy(kind="enum",memberNames="ValueToText|RangeToText|RegexToText|SpecialValue") @grafanamaturity(NeedsExpertReview) + + // Maps text values to a color or different display text and color. + // For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. + #ValueMap: { + type: #MappingType & "value" + // Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } + options: [string]: #ValueMappingResult + } @cuetsy(kind="interface") + + // Maps numerical ranges to a display text and color. + // For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. + #RangeMap: { + type: #MappingType & "range" + // Range to match against and the result to apply when the value is within the range + options: { + // Min value of the range. It can be null which means -Infinity + from: float64 | null + // Max value of the range. It can be null which means +Infinity + to: float64 | null + // Config to apply when the value is within the range + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Maps regular expressions to replacement text and a color. + // For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. + #RegexMap: { + type: #MappingType & "regex" + // Regular expression to match against and the result to apply when the value matches the regex + options: { + // Regular expression to match against + pattern: string + // Config to apply when the value matches the regex + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. + // See SpecialValueMatch to see the list of special values. + // For example, you can configure a special value mapping so that null values appear as N/A. + #SpecialValueMap: { + type: #MappingType & "special" + options: { + // Special value to match against + match: #SpecialValueMatch + // Config to apply when the value matches the special value + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + + // Special value types supported by the `SpecialValueMap` + #SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cuetsy(kind="enum",memberNames="True|False|Null|NaN|NullAndNan|Empty") + + // Result used as replacement with text and color when the value matches + #ValueMappingResult: { + // Text to display when the value matches + text?: string + // Text to use when the value matches + color?: string + // Icon to display when the value matches. Only specific visualizations. + icon?: string + // Position in the mapping array. Only used internally. + index?: int32 + } @cuetsy(kind="interface") + + // Transformations allow to manipulate data returned by a query before the system applies a visualization. + // Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, + // use the output of one transformation as the input to another transformation, etc. + #DataTransformerConfig: { + // Unique identifier of transformer + id: string + // Disabled transformations are skipped + disabled?: bool + // Optional frame matcher. When missing it will be applied to all results + filter?: #MatcherConfig + // Where to pull DataFrames from as input to transformation + topic?: "series" | "annotations" | "alertStates" // replaced with common.DataTopic + // Options to be passed to the transformer + // Valid options depend on the transformer id + options: _ + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // Counterpart for TypeScript's TimeOption type. + #TimeOption: { + display: string + from: string + to: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // Time picker configuration + // It defines the default config for the time picker and the refresh picker for the specific dashboard. + #TimePickerConfig: { + // Whether timepicker is visible or not. + hidden?: bool | *false + // Interval options available in the refresh picker dropdown. + refresh_intervals?: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] + // Quick ranges for time picker. + quick_ranges?: [...#TimeOption] + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + nowDelay?: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + // 0 for no shared crosshair or tooltip (default). + // 1 for shared crosshair. + // 2 for shared crosshair AND shared tooltip. + #DashboardCursorSync: *0 | 1 | 2 @cuetsy(kind="enum",memberNames="Off|Crosshair|Tooltip") + + // Schema for panel targets is specified by datasource + // plugins. We use a placeholder definition, which the Go + // schema loader either left open/as-is with the Base + // variant of the Dashboard and Panel families, or filled + // with types derived from plugins in the Instance variant. + // When working directly from CUE, importers can extend this + // type directly to achieve the same effect. + #Target: {...} + + // A dashboard snapshot shares an interactive dashboard publicly. + // It is a read-only version of a dashboard, and is not editable. + // It is possible to create a snapshot of a snapshot. + // Grafana strips away all sensitive information from the dashboard. + // Sensitive information stripped: queries (metric, template,annotation) and panel links. + #Snapshot: { + // Time when the snapshot was created + created: string & t.Time + // Time when the snapshot expires, default is never to expire + expires: string @grafanamaturity(NeedsExpertReview) + // Is the snapshot saved in an external grafana instance + external: bool @grafanamaturity(NeedsExpertReview) + // external url, if snapshot was shared in external grafana instance + externalUrl: string @grafanamaturity(NeedsExpertReview) + // original url, url of the dashboard that was snapshotted + originalUrl: string @grafanamaturity(NeedsExpertReview) + // Unique identifier of the snapshot + id: uint32 @grafanamaturity(NeedsExpertReview) + // Optional, defined the unique key of the snapshot, required if external is true + key: string @grafanamaturity(NeedsExpertReview) + // Optional, name of the snapshot + name: string @grafanamaturity(NeedsExpertReview) + // org id of the snapshot + orgId: uint32 @grafanamaturity(NeedsExpertReview) + // last time when the snapshot was updated + updated: string & t.Time + // url of the snapshot, if snapshot was shared internally + url?: string @grafanamaturity(NeedsExpertReview) + // user id of the snapshot creator + userId: uint32 @grafanamaturity(NeedsExpertReview) + } @grafanamaturity(NeedsExpertReview) + + // Dashboard panels are the basic visualization building blocks. + #Panel: { + // The panel plugin type id. This is used to find the plugin to display the panel. + type: string & strings.MinRunes(1) + + // Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. + id?: uint32 + + // The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. + pluginVersion?: string + + // Depends on the panel plugin. See the plugin documentation for details. + targets?: [...#Target] + + // Panel title. + title?: string + + // Panel description. + description?: string + + // Whether to display the panel without a background. + transparent?: bool | *false + + // The datasource used in all targets. + datasource?: #DataSourceRef + + // Grid position. + gridPos?: #GridPos + + // Panel links. + links?: [...#DashboardLink] + + // Name of template variable to repeat for. + repeat?: string + + // Direction to repeat in if 'repeat' is set. + // `h` for horizontal, `v` for vertical. + repeatDirection?: *"h" | "v" + + // Option for repeated panels that controls max items per row + // Only relevant for horizontally repeated panels + maxPerRow?: number + + // The maximum number of data points that the panel queries are retrieving. + maxDataPoints?: number + + // List of transformations that are applied to the panel data before rendering. + // When there are multiple transformations, Grafana applies them in the order they are listed. + // Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. + transformations?: [...#DataTransformerConfig] + + // The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables. + // This value must be formatted as a number followed by a valid time + // identifier like: "40s", "3d", etc. + // See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options + interval?: string + + // Overrides the relative time range for individual panels, + // which causes them to be different than what is selected in + // the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different + // time periods or days on the same dashboard. + // The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far), + // `now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years). + // Note: Panel time overrides have no effect when the dashboard’s time range is absolute. + // See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options + timeFrom?: string + + // Overrides the time range for individual panels by shifting its start and end relative to the time picker. + // For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`. + // Note: Panel time overrides have no effect when the dashboard’s time range is absolute. + // See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options + timeShift?: string + + // Controls if the timeFrom or timeShift overrides are shown in the panel header + hideTimeOverride?: bool + + // Dynamically load the panel + libraryPanel?: #LibraryPanelRef + + // Sets panel queries cache timeout. + cacheTimeout?: string + + // Overrides the data source configured time-to-live for a query cache item in milliseconds + queryCachingTTL?: number + + // It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. + options?: {...} @grafanamaturity(NeedsExpertReview) + + // Field options allow you to change how the data is displayed in your visualizations. + fieldConfig?: #FieldConfigSource + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. + // Each column within this structure is called a field. A field can represent a single time series or table column. + // Field options allow you to change how the data is displayed in your visualizations. + #FieldConfigSource: { + // Defaults are the options applied to all fields. + defaults: #FieldConfig + // Overrides are the options applied to specific fields overriding the defaults. + overrides: [...{ + matcher: #MatcherConfig + properties: [...#DynamicConfigValue] + }] @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // A library panel is a reusable panel that you can use in any dashboard. + // When you make a change to a library panel, that change propagates to all instances of where the panel is used. + // Library panels streamline reuse of panels across multiple dashboards. + #LibraryPanelRef: { + // Library panel name + name: string + // Library panel uid + uid: string + } @cuetsy(kind="interface") + + // Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. + // It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. + #MatcherConfig: { + // The matcher id. This is used to find the matcher implementation from registry. + id: string | *"" @grafanamaturity(NeedsExpertReview) + // The matcher options. This is specific to the matcher implementation. + options?: _ @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + + #DynamicConfigValue: { + id: string | *"" @grafanamaturity(NeedsExpertReview) + value?: _ @grafanamaturity(NeedsExpertReview) + } + + // The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. + // Each column within this structure is called a field. A field can represent a single time series or table column. + // Field options allow you to change how the data is displayed in your visualizations. + #FieldConfig: { + // The display value for this field. This supports template variables blank is auto + displayName?: string @grafanamaturity(NeedsExpertReview) + + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string @grafanamaturity(NeedsExpertReview) + + // Human readable field metadata + description?: string @grafanamaturity(NeedsExpertReview) + + // An explicit path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string @grafanamaturity(NeedsExpertReview) + + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool @grafanamaturity(NeedsExpertReview) + + // True if data source field supports ad-hoc filters + filterable?: bool @grafanamaturity(NeedsExpertReview) + + // Unit a field should use. The unit you select is applied to all fields except time. + // You can use the units ID availables in Grafana or a custom unit. + // Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts + // As custom unit, you can use the following formats: + // `suffix:` for custom unit that should go after value. + // `prefix:` for custom unit that should go before value. + // `time:` For custom date time formats type for example `time:YYYY-MM-DD`. + // `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. + // `count:` for a custom count unit. + // `currency:` for custom a currency unit. + unit?: string @grafanamaturity(NeedsExpertReview) + + // Specify the number of decimals Grafana includes in the rendered value. + // If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. + // For example 1.1234 will display as 1.12 and 100.456 will display as 100. + // To display all decimals, set the unit to `String`. + decimals?: number @grafanamaturity(NeedsExpertReview) + + // The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + min?: number @grafanamaturity(NeedsExpertReview) + // The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + max?: number @grafanamaturity(NeedsExpertReview) + + // Convert input values into a display string + mappings?: [...#ValueMapping] @grafanamaturity(NeedsExpertReview) + + // Map numeric values to states + thresholds?: #ThresholdsConfig @grafanamaturity(NeedsExpertReview) + + // Panel color configuration + color?: #FieldColor + + // The behavior when clicking on a result + links?: [...] @grafanamaturity(NeedsExpertReview) + + // Alternative to empty string + noValue?: string @grafanamaturity(NeedsExpertReview) + + // custom is specified by the FieldConfig field + // in panel plugin schemas. + custom?: {...} @grafanamaturity(NeedsExpertReview) + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview) + + // Row panel + #RowPanel: { + // The panel type + type: "row" + + // Whether this row should be collapsed or not. + collapsed: bool | *false + + // Row title + title?: string + + // Name of default datasource for the row + datasource?: #DataSourceRef + + // Row grid position + gridPos?: #GridPos + + // Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. + id: uint32 + + // List of panels in the row + panels: [...#Panel] + + // Name of template variable to repeat for. + repeat?: string + } @cuetsy(kind="interface") @grafana(TSVeneer="type") + } +}, +] diff --git a/apps/dashboard/pkg/apis/dashboard/v1alpha1/validation.go b/apps/dashboard/pkg/apis/dashboard/v1alpha1/validation.go new file mode 100644 index 00000000000..37117b10108 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v1alpha1/validation.go @@ -0,0 +1,90 @@ +package v1alpha1 + +import ( + _ "embed" + json "encoding/json" + fmt "fmt" + "strings" + "sync" + + "k8s.io/apimachinery/pkg/util/validation/field" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/errors" + cuejson "cuelang.org/go/encoding/json" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" +) + +func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorList, field.ErrorList) { + var schemaVersionError field.ErrorList + schemaVersion := schemaversion.GetSchemaVersion(obj.Spec.Object) + if schemaVersion != schemaversion.LATEST_VERSION { + schemaVersionError = field.ErrorList{field.Invalid(field.NewPath("spec", "schemaVersion"), field.OmitValueType{}, fmt.Sprintf("Schema version %d is not supported - please upgrade to %d", schemaVersion, schemaversion.LATEST_VERSION))} + if !forceValidation { + return nil, schemaVersionError + } + } + + data, err := json.Marshal(obj.Spec.Object) + if err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec"), field.OmitValueType{}, err.Error()), + }, schemaVersionError + } + + if err := cuejson.Validate(data, getCueSchema()); err != nil { + errs := field.ErrorList{} + + for _, e := range errors.Errors(err) { + if + // We don't want to return confusing "empty disjunction" errors, + // because the users don't necessarily understand what to do with them. + // For empty disjunctions, CUE will also return more specific errors, + // so we can safely ignore the generic ones. + strings.Contains(e.Error(), "disjunction") || + // We don't want to return errors about unknown fields either. + strings.Contains(e.Error(), "field not allowed") { + continue + } + + // We want to manually format the error message, + // because e.Error() contains the full CUE path. + format, args := e.Msg() + + errs = append(errs, field.Invalid( + field.NewPath(formatErrorPath(e.Path())), + field.OmitValueType{}, + fmt.Sprintf(format, args...), + )) + } + + return errs, schemaVersionError + } + + return nil, schemaVersionError +} + +func formatErrorPath(path []string) string { + // omitting the "lineage.schemas[0].schema.spec" prefix here. + return strings.Join(path[4:], ".") +} + +var ( + compiledSchema cue.Value + getSchemaOnce sync.Once +) + +//go:embed dashboard_kind.cue +var schemaSource string + +func getCueSchema() cue.Value { + getSchemaOnce.Do(func() { + cueCtx := cuecontext.New() + compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + cue.ParsePath("lineage.schemas[0].schema.spec"), + ) + }) + + return compiledSchema +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue new file mode 100644 index 00000000000..c1acdf81a2f --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -0,0 +1,970 @@ +// This file is managed by grafana-app-sdk - DO NOT EDIT MANUALLY +// Source: apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +// To sync changes, run: make generate in apps/dashboard + +package v2alpha1 + +DashboardSpec: { + annotations: [...AnnotationQueryKind] | *[] + + // Configuration of dashboard cursor sync behavior. + // "Off" for no shared crosshair or tooltip (default). + // "Crosshair" for shared crosshair. + // "Tooltip" for shared crosshair AND shared tooltip. + cursorSync: DashboardCursorSync + + // Description of dashboard. + description?: string + + // Whether a dashboard is editable or not. + editable?: bool | *true + + elements: [ElementReference.name]: Element | *{} + + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind + + // Links with references to other dashboards or external websites. + links: [...DashboardLink] | *[] + + // When set to true, the dashboard will redraw panels at an interval matching the pixel width. + // This will keep data "moving left" regardless of the query refresh rate. This setting helps + // avoid dashboards presenting stale live data. + liveNow?: bool + + // When set to true, the dashboard will load all panels in the dashboard when it's loaded. + preload: bool | *false + + // Plugins only. The version of the dashboard installed together with the plugin. + // This is used to determine if the dashboard should be updated when the plugin is updated. + revision?: uint16 + + // Tags associated with dashboard. + tags: [...string] | *[] + + timeSettings: TimeSettingsSpec + + // Title of dashboard. + title: string + + // Configured template variables. + variables: [...VariableKind] | *[] +} + +// Supported dashboard elements +Element: PanelKind | LibraryPanelKind // |* more element types in the future + +LibraryPanelKind: { + kind: "LibraryPanel" + spec: LibraryPanelKindSpec +} + +LibraryPanelKindSpec: { + // Panel ID for the library panel in the dashboard + id: number + // Title for the library panel in the dashboard + title: string + + libraryPanel: LibraryPanelRef +} + +// A library panel is a reusable panel that you can use in any dashboard. +// When you make a change to a library panel, that change propagates to all instances of where the panel is used. +// Library panels streamline reuse of panels across multiple dashboards. +LibraryPanelRef: { + // Library panel name + name: string + // Library panel uid + uid: string +} + +AnnotationPanelFilter: { + // Should the specified panels be included or excluded + exclude?: bool | *false + + // Panel IDs that should be included or excluded + ids: [...uint8] +} + +// "Off" for no shared crosshair or tooltip (default). +// "Crosshair" for shared crosshair. +// "Tooltip" for shared crosshair AND shared tooltip. +DashboardCursorSync: "Crosshair" | "Tooltip" | *"Off" + +// Links with references to other dashboards or external resources +DashboardLink: { + // Title to display with the link + title: string + // Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) + // FIXME: The type is generated as `type: DashboardLinkType | dashboardLinkType.Link;` but it should be `type: DashboardLinkType` + type: DashboardLinkType + // Icon name to be displayed with the link + icon: string + // Tooltip to display when the user hovers their mouse over it + tooltip: string + // Link URL. Only required/valid if the type is link + url?: string + // List of tags to limit the linked dashboards. If empty, all dashboards will be displayed. Only valid if the type is dashboards + tags: [...string] | *[] + // If true, all dashboards links will be displayed in a dropdown. If false, all dashboards links will be displayed side by side. Only valid if the type is dashboards + asDropdown: bool | *false + // If true, the link will be opened in a new tab + targetBlank: bool | *false + // If true, includes current template variables values in the link as query params + includeVars: bool | *false + // If true, includes current time range in the link as query params + keepTime: bool | *false +} + +DataSourceRef: { + // The plugin type-id + type?: string + + // Specific datasource instance + uid?: string +} + +// A topic is attached to DataFrame metadata in query results. +// This specifies where the data should be used. +DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames="Series|Annotations|AlertStates") + +// Transformations allow to manipulate data returned by a query before the system applies a visualization. +// Using transformations you can: rename fields, join time series data, perform mathematical operations across queries, +// use the output of one transformation as the input to another transformation, etc. +DataTransformerConfig: { + // Unique identifier of transformer + id: string + // Disabled transformations are skipped + disabled?: bool + // Optional frame matcher. When missing it will be applied to all results + filter?: MatcherConfig + // Where to pull DataFrames from as input to transformation + topic?: DataTopic + // Options to be passed to the transformer + // Valid options depend on the transformer id + options: _ +} + +DataLink: { + title: string + url: string + targetBlank?: bool +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +FieldConfigSource: { + // Defaults are the options applied to all fields. + defaults: FieldConfig + // Overrides are the options applied to specific fields overriding the defaults. + overrides: [...{ + matcher: MatcherConfig + properties: [...DynamicConfigValue] + }] +} + +// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results. +// Each column within this structure is called a field. A field can represent a single time series or table column. +// Field options allow you to change how the data is displayed in your visualizations. +FieldConfig: { + // The display value for this field. This supports template variables blank is auto + displayName?: string + + // This can be used by data sources that return and explicit naming structure for values and labels + // When this property is configured, this value is used rather than the default naming strategy. + displayNameFromDS?: string + + // Human readable field metadata + description?: string + + // An explicit path to the field in the datasource. When the frame meta includes a path, + // This will default to `${frame.meta.path}/${field.name} + // + // When defined, this value can be used as an identifier within the datasource scope, and + // may be used to update the results + path?: string + + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool + + // True if data source field supports ad-hoc filters + filterable?: bool + + // Unit a field should use. The unit you select is applied to all fields except time. + // You can use the units ID availables in Grafana or a custom unit. + // Available units in Grafana: https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/valueFormats/categories.ts + // As custom unit, you can use the following formats: + // `suffix:` for custom unit that should go after value. + // `prefix:` for custom unit that should go before value. + // `time:` For custom date time formats type for example `time:YYYY-MM-DD`. + // `si:` for custom SI units. For example: `si: mF`. This one is a bit more advanced as you can specify both a unit and the source data scale. So if your source data is represented as milli (thousands of) something prefix the unit with that SI scale character. + // `count:` for a custom count unit. + // `currency:` for custom a currency unit. + unit?: string + + // Specify the number of decimals Grafana includes in the rendered value. + // If you leave this field blank, Grafana automatically truncates the number of decimals based on the value. + // For example 1.1234 will display as 1.12 and 100.456 will display as 100. + // To display all decimals, set the unit to `String`. + decimals?: number + + // The minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + min?: number + // The maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields. + max?: number + + // Convert input values into a display string + mappings?: [...ValueMapping] + + // Map numeric values to states + thresholds?: ThresholdsConfig + + // Panel color configuration + color?: FieldColor + + // The behavior when clicking on a result + links?: [...] + + // Alternative to empty string + noValue?: string + + // custom is specified by the FieldConfig field + // in panel plugin schemas. + custom?: {...} +} + +DynamicConfigValue: { + id: string | *"" + value?: _ +} + +// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation. +// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type. +MatcherConfig: { + // The matcher id. This is used to find the matcher implementation from registry. + id: string | *"" + // The matcher options. This is specific to the matcher implementation. + options?: _ +} + +Threshold: { + value: number + color: string +} + +ThresholdsMode: "absolute" | "percentage" + +ThresholdsConfig: { + mode: ThresholdsMode + steps: [...Threshold] +} + +ValueMapping: ValueMap | RangeMap | RegexMap | SpecialValueMap + +// Supported value mapping types +// `value`: Maps text values to a color or different display text and color. For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +// `range`: Maps numerical ranges to a display text and color. For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +// `regex`: Maps regular expressions to replacement text and a color. For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +// `special`: Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. See SpecialValueMatch to see the list of special values. For example, you can configure a special value mapping so that null values appear as N/A. +MappingType: "value" | "range" | "regex" | "special" + +// Maps text values to a color or different display text and color. +// For example, you can configure a value mapping so that all instances of the value 10 appear as Perfection! rather than the number. +ValueMap: { + type: MappingType & "value" + // Map with : ValueMappingResult. For example: { "10": { text: "Perfection!", color: "green" } } + options: [string]: ValueMappingResult +} + +// Maps numerical ranges to a display text and color. +// For example, if a value is within a certain range, you can configure a range value mapping to display Low or High rather than the number. +RangeMap: { + type: MappingType & "range" + // Range to match against and the result to apply when the value is within the range + options: { + // Min value of the range. It can be null which means -Infinity + from: float64 | null + // Max value of the range. It can be null which means +Infinity + to: float64 | null + // Config to apply when the value is within the range + result: ValueMappingResult + } +} + +// Maps regular expressions to replacement text and a color. +// For example, if a value is www.example.com, you can configure a regex value mapping so that Grafana displays www and truncates the domain. +RegexMap: { + type: MappingType & "regex" + // Regular expression to match against and the result to apply when the value matches the regex + options: { + // Regular expression to match against + pattern: string + // Config to apply when the value matches the regex + result: ValueMappingResult + } +} + +// Maps special values like Null, NaN (not a number), and boolean values like true and false to a display text and color. +// See SpecialValueMatch to see the list of special values. +// For example, you can configure a special value mapping so that null values appear as N/A. +SpecialValueMap: { + type: MappingType & "special" + options: { + // Special value to match against + match: SpecialValueMatch + // Config to apply when the value matches the special value + result: ValueMappingResult + } +} + +// Special value types supported by the `SpecialValueMap` +SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cog(kind="enum",memberNames="True|False|Null|NaN|NullAndNaN|Empty") + +// Result used as replacement with text and color when the value matches +ValueMappingResult: { + // Text to display when the value matches + text?: string + // Text to use when the value matches + color?: string + // Icon to display when the value matches. Only specific visualizations. + icon?: string + // Position in the mapping array. Only used internally. + index?: int32 +} + +// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value. +// Continuous color interpolates a color using the percentage of a value relative to min and max. +// Accepted values are: +// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold +// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations +// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations +// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode +// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode +// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode +// `continuous-YlRd`: Continuous Yellow-Red palette mode +// `continuous-BlPu`: Continuous Blue-Purple palette mode +// `continuous-YlBl`: Continuous Yellow-Blue palette mode +// `continuous-blues`: Continuous Blue palette mode +// `continuous-reds`: Continuous Red palette mode +// `continuous-greens`: Continuous Green palette mode +// `continuous-purples`: Continuous Purple palette mode +// `shades`: Shades of a single color. Specify a single color, useful in an override rule. +// `fixed`: Fixed color mode. Specify a single color, useful in an override rule. +FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" + +// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value. +FieldColorSeriesByMode: "min" | "max" | "last" + +// Map a field to a color. +FieldColor: { + // The main color scheme mode. + mode: FieldColorModeId + // The fixed color value for fixed or shades color modes. + fixedColor?: string + // Some visualizations need to know how to assign a series color from by value color schemes. + seriesBy?: FieldColorSeriesByMode +} + +// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource) +DashboardLinkType: "link" | "dashboards" + +// --- Common types --- +Kind: { + kind: string + spec: _ + metadata?: _ +} + +// --- Kinds --- +VizConfigSpec: { + pluginVersion: string + options: [string]: _ + fieldConfig: FieldConfigSource +} + +VizConfigKind: { + // The kind of a VizConfigKind is the plugin ID + kind: string + spec: VizConfigSpec +} + +AnnotationQuerySpec: { + datasource?: DataSourceRef + query?: DataQueryKind + enable: bool + hide: bool + iconColor: string + name: string + builtIn?: bool | *false + filter?: AnnotationPanelFilter + options?: [string]: _ //Catch-all field for datasource-specific properties +} + +AnnotationQueryKind: { + kind: "AnnotationQuery" + spec: AnnotationQuerySpec +} + +QueryOptionsSpec: { + timeFrom?: string + maxDataPoints?: int + timeShift?: string + queryCachingTTL?: int + interval?: string + cacheTimeout?: string + hideTimeOverride?: bool +} + +DataQueryKind: { + // The kind of a DataQueryKind is the datasource type + kind: string + spec: [string]: _ +} + +PanelQuerySpec: { + query: DataQueryKind + datasource?: DataSourceRef + + refId: string + hidden: bool +} + +PanelQueryKind: { + kind: "PanelQuery" + spec: PanelQuerySpec +} + +TransformationKind: { + // The kind of a TransformationKind is the transformation ID + kind: string + spec: DataTransformerConfig +} + +QueryGroupSpec: { + queries: [...PanelQueryKind] + transformations: [...TransformationKind] + queryOptions: QueryOptionsSpec +} + +QueryGroupKind: { + kind: "QueryGroup" + spec: QueryGroupSpec +} + +TimeRangeOption: { + display: string | *"Last 6 hours" + from: string | *"now-6h" + to: string | *"now" +} + +// Time configuration +// It defines the default time config for the time picker, the refresh picker for the specific dashboard. +TimeSettingsSpec: { + // Timezone of dashboard. Accepted values are IANA TZDB zone ID or "browser" or "utc". + timezone?: string | *"browser" + // Start time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + from: string | *"now-6h" + // End time range for dashboard. + // Accepted values are relative time strings like "now-6h" or absolute time strings like "2020-07-10T08:00:00.000Z". + to: string | *"now" + // Refresh rate of dashboard. Represented via interval string, e.g. "5s", "1m", "1h", "1d". + autoRefresh: string | *"" // v1: refresh + // Interval options available in the refresh picker dropdown. + autoRefreshIntervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"] // v1: timepicker.refresh_intervals + // Selectable options available in the time picker dropdown. Has no effect on provisioned dashboard. + quickRanges?: [...TimeRangeOption] // v1: timepicker.quick_ranges , not exposed in the UI + // Whether timepicker is visible or not. + hideTimepicker: bool | *false // v1: timepicker.hidden + // Day when the week starts. Expressed by the name of the day in lowercase, e.g. "monday". + weekStart?: "saturday" | "monday" | "sunday" + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth: int | *0 + // Override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. + nowDelay?: string // v1: timepicker.nowDelay +} + +RepeatMode: "variable" // other repeat modes will be added in the future: label, frame + +RepeatOptions: { + mode: RepeatMode + value: string + direction?: "h" | "v" + maxPerRow?: int +} + +RowRepeatOptions: { + mode: RepeatMode + value: string +} + +TabRepeatOptions: { + mode: RepeatMode + value: string +} + +AutoGridRepeatOptions: { + mode: RepeatMode + value: string +} + +GridLayoutItemSpec: { + x: int + y: int + width: int + height: int + element: ElementReference // reference to a PanelKind from dashboard.spec.elements Expressed as JSON Schema reference + repeat?: RepeatOptions +} + +GridLayoutItemKind: { + kind: "GridLayoutItem" + spec: GridLayoutItemSpec +} + +GridLayoutRowKind: { + kind: "GridLayoutRow" + spec: GridLayoutRowSpec +} + +GridLayoutRowSpec: { + y: int + collapsed: bool + title: string + elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. + repeat?: RowRepeatOptions +} + +GridLayoutSpec: { + items: [...GridLayoutItemKind | GridLayoutRowKind] +} + +GridLayoutKind: { + kind: "GridLayout" + spec: GridLayoutSpec +} + +RowsLayoutKind: { + kind: "RowsLayout" + spec: RowsLayoutSpec +} + +RowsLayoutSpec: { + rows: [...RowsLayoutRowKind] +} + +RowsLayoutRowKind: { + kind: "RowsLayoutRow" + spec: RowsLayoutRowSpec +} + +RowsLayoutRowSpec: { + title?: string + collapse?: bool + hideHeader?: bool + fillScreen?: bool + conditionalRendering?: ConditionalRenderingGroupKind + repeat?: RowRepeatOptions + layout: GridLayoutKind | AutoGridLayoutKind | TabsLayoutKind | RowsLayoutKind +} + +AutoGridLayoutKind: { + kind: "AutoGridLayout" + spec: AutoGridLayoutSpec +} + +AutoGridLayoutSpec: { + maxColumnCount?: number | *3 + columnWidthMode: "narrow" | *"standard" | "wide" | "custom" + columnWidth?: number + rowHeightMode: "short" | *"standard" | "tall" | "custom" + rowHeight?: number + fillScreen?: bool | *false + items: [...AutoGridLayoutItemKind] +} + +AutoGridLayoutItemKind: { + kind: "AutoGridLayoutItem" + spec: AutoGridLayoutItemSpec +} + +AutoGridLayoutItemSpec: { + element: ElementReference + repeat?: AutoGridRepeatOptions + conditionalRendering?: ConditionalRenderingGroupKind +} + +TabsLayoutKind: { + kind: "TabsLayout" + spec: TabsLayoutSpec +} + +TabsLayoutSpec: { + tabs: [...TabsLayoutTabKind] +} + +TabsLayoutTabKind: { + kind: "TabsLayoutTab" + spec: TabsLayoutTabSpec +} + +TabsLayoutTabSpec: { + title?: string + layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind + conditionalRendering?: ConditionalRenderingGroupKind + repeat?: TabRepeatOptions +} + +PanelSpec: { + id: number + title: string + description: string + links: [...DataLink] + data: QueryGroupKind + vizConfig: VizConfigKind + transparent?: bool +} + +PanelKind: { + kind: "Panel" + spec: PanelSpec +} + +ElementReference: { + kind: "ElementReference" + name: string +} + +// Start FIXME: variables - in CUE PR - this are things that should be added into the cue schema +// TODO: properties such as `hide`, `skipUrlSync`, `multi` are type boolean, and in the old schema they are conditional, +// should we make them conditional in the new schema as well? or should we make them required but default to false? + +// Variable types +VariableValue: VariableValueSingle | [...VariableValueSingle] + +VariableValueSingle: string | bool | number | CustomVariableValue + +// Custom formatter variable +CustomFormatterVariable: { + name: string + type: VariableType + multi: bool + includeAll: bool +} + +// Custom variable value +CustomVariableValue: { + // The format name or function used in the expression + formatter: *null | string | VariableCustomFormatterFn +} + +// Custom formatter function +VariableCustomFormatterFn: { + value: _ + legacyVariableModel: { + name: string + type: VariableType + multi: bool + includeAll: bool + } + legacyDefaultFormatter?: VariableCustomFormatterFn +} + +// Dashboard variable type +// `query`: Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. +// `adhoc`: Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). +// `constant`: Define a hidden constant. +// `datasource`: Quickly change the data source for an entire dashboard. +// `interval`: Interval variables represent time spans. +// `textbox`: Display a free text input field with an optional default value. +// `custom`: Define the variable options manually using a comma-separated list. +// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables +VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | + "system" | "snapshot" + +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind + +// Sort variable options +// Accepted values are: +// `disabled`: No sorting +// `alphabeticalAsc`: Alphabetical ASC +// `alphabeticalDesc`: Alphabetical DESC +// `numericalAsc`: Numerical ASC +// `numericalDesc`: Numerical DESC +// `alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC +// `alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC +// `naturalAsc`: Natural ASC +// `naturalDesc`: Natural DESC +// VariableSort enum with default value +VariableSort: "disabled" | "alphabeticalAsc" | "alphabeticalDesc" | "numericalAsc" | "numericalDesc" | "alphabeticalCaseInsensitiveAsc" | "alphabeticalCaseInsensitiveDesc" | "naturalAsc" | "naturalDesc" + +// Options to config when to refresh a variable +// `never`: Never refresh the variable +// `onDashboardLoad`: Queries the data source every time the dashboard loads. +// `onTimeRangeChanged`: Queries the data source when the dashboard time range changes. +VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged" + +// Determine if the variable shows on dashboard +// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing). +VariableHide: *"dontHide" | "hideLabel" | "hideVariable" + +// FIXME: should we introduce this? --- Variable value option +VariableValueOption: { + label: string + value: VariableValueSingle + group?: string +} + +// Variable option specification +VariableOption: { + // Whether the option is selected or not + selected?: bool + // Text to be displayed for the option + text: string | [...string] + // Value of the option + value: string | [...string] +} + +// Query variable specification +QueryVariableSpec: { + name: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + label?: string + hide: VariableHide + refresh: VariableRefresh + skipUrlSync: bool | *false + description?: string + datasource?: DataSourceRef + query: DataQueryKind + regex: string | *"" + sort: VariableSort + definition?: string + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + placeholder?: string +} + +// Query variable kind +QueryVariableKind: { + kind: "QueryVariable" + spec: QueryVariableSpec +} + +// Text variable specification +TextVariableSpec: { + name: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + query: string | *"" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Text variable kind +TextVariableKind: { + kind: "TextVariable" + spec: TextVariableSpec +} + +// Constant variable specification +ConstantVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Constant variable kind +ConstantVariableKind: { + kind: "ConstantVariable" + spec: ConstantVariableSpec +} + +// Datasource variable specification +DatasourceVariableSpec: { + name: string | *"" + pluginId: string | *"" + refresh: VariableRefresh + regex: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Datasource variable kind +DatasourceVariableKind: { + kind: "DatasourceVariable" + spec: DatasourceVariableSpec +} + +// Interval variable specification +IntervalVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + auto: bool | *false + auto_min: string | *"" + auto_count: int | *0 + refresh: VariableRefresh + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Interval variable kind +IntervalVariableKind: { + kind: "IntervalVariable" + spec: IntervalVariableSpec +} + +// Custom variable specification +CustomVariableSpec: { + name: string | *"" + query: string | *"" + current: VariableOption + options: [...VariableOption] | *[] + multi: bool | *false + includeAll: bool | *false + allValue?: string + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Custom variable kind +CustomVariableKind: { + kind: "CustomVariable" + spec: CustomVariableSpec +} + +// GroupBy variable specification +GroupByVariableSpec: { + name: string | *"" + datasource?: DataSourceRef + current: VariableOption | *{ + text: "" + value: "" + } + options: [...VariableOption] | *[] + multi: bool | *false + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Group variable kind +GroupByVariableKind: { + kind: "GroupByVariable" + spec: GroupByVariableSpec +} + +// Adhoc variable specification +AdhocVariableSpec: { + name: string | *"" + datasource?: DataSourceRef + baseFilters: [...AdHocFilterWithLabels] | *[] + filters: [...AdHocFilterWithLabels] | *[] + defaultKeys: [...MetricFindValue] | *[] + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +// Define the MetricFindValue type +MetricFindValue: { + text: string + value?: string | number + group?: string + expandable?: bool +} + +// Define the AdHocFilterWithLabels type +AdHocFilterWithLabels: { + key: string + operator: string + value: string + values?: [...string] + keyLabel?: string + valueLabels?: [...string] + forceEdit?: bool + // @deprecated + condition?: string +} + +// Adhoc variable kind +AdhocVariableKind: { + kind: "AdhocVariable" + spec: AdhocVariableSpec +} + +ConditionalRenderingGroupKind: { + kind: "ConditionalRenderingGroup" + spec: ConditionalRenderingGroupSpec +} + +ConditionalRenderingGroupSpec: { + visibility: "show" | "hide" + condition: "and" | "or" + items: [...ConditionalRenderingVariableKind | ConditionalRenderingDataKind | ConditionalRenderingTimeRangeSizeKind] +} + +ConditionalRenderingVariableKind: { + kind: "ConditionalRenderingVariable" + spec: ConditionalRenderingVariableSpec +} + +ConditionalRenderingVariableSpec: { + variable: string + operator: "equals" | "notEquals" + value: string +} + +ConditionalRenderingDataKind: { + kind: "ConditionalRenderingData" + spec: ConditionalRenderingDataSpec +} + +ConditionalRenderingDataSpec: { + value: bool +} + +ConditionalRenderingTimeRangeSizeKind: { + kind: "ConditionalRenderingTimeRangeSize" + spec: ConditionalRenderingTimeRangeSizeSpec +} + +ConditionalRenderingTimeRangeSizeSpec: { + value: string +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index f104c3fdf45..70faef285ea 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -90,9 +90,9 @@ func NewDashboardAnnotationPanelFilter() *DashboardAnnotationPanelFilter { type DashboardDashboardCursorSync string const ( - DashboardDashboardCursorSyncOff DashboardDashboardCursorSync = "Off" DashboardDashboardCursorSyncCrosshair DashboardDashboardCursorSync = "Crosshair" DashboardDashboardCursorSyncTooltip DashboardDashboardCursorSync = "Tooltip" + DashboardDashboardCursorSyncOff DashboardDashboardCursorSync = "Off" ) // Supported dashboard elements @@ -1174,7 +1174,10 @@ func NewDashboardTimeSettingsSpec() *DashboardTimeSettingsSpec { Timezone: (func(input string) *string { return &input })("browser"), From: "now-6h", To: "now", + AutoRefresh: "", AutoRefreshIntervals: []string{"5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"}, + HideTimepicker: false, + FiscalYearStartMonth: 0, } } @@ -1694,7 +1697,6 @@ func NewDashboardMetricFindValue() *DashboardMetricFindValue { // +k8s:openapi-gen=true type DashboardSpec struct { - // Title of dashboard. Annotations []DashboardAnnotationQueryKind `json:"annotations"` // Configuration of dashboard cursor sync behavior. // "Off" for no shared crosshair or tooltip (default). @@ -1730,8 +1732,10 @@ type DashboardSpec struct { // NewDashboardSpec creates a new DashboardSpec object. func NewDashboardSpec() *DashboardSpec { return &DashboardSpec{ + CursorSync: DashboardDashboardCursorSyncOff, Editable: (func(input bool) *bool { return &input })(true), Layout: *NewDashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(), + Preload: false, TimeSettings: *NewDashboardTimeSettingsSpec(), } } diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go new file mode 100644 index 00000000000..7445000d7f8 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/validation.go @@ -0,0 +1,84 @@ +package v2alpha1 + +import ( + _ "embed" + json "encoding/json" + fmt "fmt" + "strings" + "sync" + + "k8s.io/apimachinery/pkg/util/validation/field" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/errors" + cuejson "cuelang.org/go/encoding/json" +) + +func ValidateDashboardSpec(obj *Dashboard) field.ErrorList { + data, err := json.Marshal(obj.Spec) + if err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec"), field.OmitValueType{}, err.Error()), + } + } + + if err := cuejson.Validate(data, getCueSchema()); err != nil { + errs := field.ErrorList{} + + for _, e := range errors.Errors(err) { + if + // We don't want to return confusing "empty disjunction" errors, + // because the users don't necessarily understand what to do with them. + // For empty disjunctions, CUE will also return more specific errors, + // so we can safely ignore the generic ones. + strings.Contains(e.Error(), "disjunction") || + // We don't want to return errors about unknown fields either. + strings.Contains(e.Error(), "field not allowed") { + continue + } + + if strings.Contains(e.Error(), "mismatched types null and list") { + // Go populates empty slices as nil, which the cue validator does not like + continue + } + + // We want to manually format the error message, + // because e.Error() contains the full CUE path. + format, args := e.Msg() + + errs = append(errs, field.Invalid( + field.NewPath(formatErrorPath(e.Path())), + field.OmitValueType{}, + fmt.Sprintf(format, args...), + )) + } + + return errs + } + + return nil +} + +func formatErrorPath(path []string) string { + return strings.Join(path, ".") +} + +var ( + compiledSchema cue.Value + getSchemaOnce sync.Once +) + +//go:embed dashboard_spec.cue +var schemaSource string + +func getCueSchema() cue.Value { + getSchemaOnce.Do(func() { + cueCtx := cuecontext.New() + compiledSchema = cueCtx.CompileString(schemaSource).LookupPath( + cue.ParsePath("DashboardSpec"), + ) + }) + + return compiledSchema +} diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index e2f3b302f99..4f548196584 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -3678,8 +3678,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref common.ReferenceCallba Properties: map[string]spec.Schema{ "annotations": { SchemaProps: spec.SchemaProps{ - Description: "Title of dashboard.", - Type: []string{"array"}, + Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 9957f356730..639657d1083 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -49,7 +49,7 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -64,18 +64,18 @@ require ( go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 19a00208ff8..3afdcda6ee6 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGC github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= @@ -155,34 +155,34 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -193,8 +193,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 8905d10d077..f63b1190197 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -50,7 +50,7 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -65,18 +65,18 @@ require ( go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 19a00208ff8..3afdcda6ee6 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGC github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= @@ -155,34 +155,34 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -193,8 +193,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/conf/defaults.ini b/conf/defaults.ini index 1678a620278..e5b1f010c8f 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1067,8 +1067,8 @@ user_identity_federated_credential_audience = username_assertion = # Set the plugins that will receive Azure settings for each request (via plugin context) -# By default this will include all Grafana Labs owned Azure plugins, or those that make use of Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL). -forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql +# By default this will include all Grafana Labs owned Azure plugins, or those that make use of Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL, Azure Prometheus). +forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql, grafana-azureprometheus-datasource # Specifies whether Entra password auth can be used for the MSSQL data source # Disabled by default, needs to be explicitly enabled diff --git a/conf/provisioning/sample/dashboard-v2.json b/conf/provisioning/sample/dashboard-v2.json new file mode 100644 index 00000000000..ad4671943c3 --- /dev/null +++ b/conf/provisioning/sample/dashboard-v2.json @@ -0,0 +1,246 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "sample-dash-v2" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts" + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "Simle timeseries", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.0-pre" + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "Simple stat", + "vizConfig": { + "kind": "stat", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.0-pre" + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "v2alpha1 dashboard", + "variables": [] + } +} diff --git a/conf/sample.ini b/conf/sample.ini index e23dd0631c5..39afdec6fea 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1048,7 +1048,7 @@ # Set the plugins that will receive Azure settings for each request (via plugin context) # By default this will include all Grafana Labs owned Azure plugins, or those that make use of Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL). -;forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql +;forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql, grafana-azureprometheus-datasourc # Specifies whether Entra password auth can be used for the MSSQL data source # Disabled by default, needs to be explicitly enabled diff --git a/devenv/dev-dashboards/all-panels.json b/devenv/dev-dashboards/all-panels.json index b9c95e8663e..c6d93b191fc 100644 --- a/devenv/dev-dashboards/all-panels.json +++ b/devenv/dev-dashboards/all-panels.json @@ -3,7 +3,6 @@ "list": [ { "builtIn": 1, - "datasource": "-- Grafana --", "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", @@ -883,7 +882,7 @@ } ], "refresh": "", - "schemaVersion": 33, + "schemaVersion": 36, "tags": [ "gdev", "panel-tests", diff --git a/devenv/dev-dashboards/panel-text/text-options.json b/devenv/dev-dashboards/panel-text/text-options.json index 1d9818ac50d..af1c758a86e 100644 --- a/devenv/dev-dashboards/panel-text/text-options.json +++ b/devenv/dev-dashboards/panel-text/text-options.json @@ -169,7 +169,7 @@ "type": "text" } ], - "refresh": false, + "refresh": "", "schemaVersion": 37, "tags": [], "templating": { diff --git a/docs/docs.mk b/docs/docs.mk index 09308dc8604..de9c4144bc1 100644 --- a/docs/docs.mk +++ b/docs/docs.mk @@ -31,9 +31,6 @@ ifeq ($(PROJECTS),) $(error "PROJECTS variable must be defined in variables.mk") endif -# First project is considered the primary one used for doc-validator. -PRIMARY_PROJECT := $(subst /,-,$(firstword $(subst :, ,$(firstword $(PROJECTS))))) - # Host port to publish container port to. ifeq ($(origin DOCS_HOST_PORT), undefined) export DOCS_HOST_PORT := 3002 @@ -44,12 +41,7 @@ ifeq ($(origin DOCS_IMAGE), undefined) export DOCS_IMAGE := grafana/docs-base:latest endif -# Container image used for doc-validator linting. -ifeq ($(origin DOC_VALIDATOR_IMAGE), undefined) -export DOC_VALIDATOR_IMAGE := grafana/doc-validator:latest -endif - -# Container image used for vale linting. +# Container image used for Vale linting. ifeq ($(origin VALE_IMAGE), undefined) export VALE_IMAGE := grafana/vale:latest endif @@ -99,14 +91,6 @@ docs-debug: ## Run Hugo web server with debugging enabled. TODO: support all SER docs-debug: make-docs WEBSITE_EXEC='hugo server --bind 0.0.0.0 --port 3002 --debug' $(CURDIR)/make-docs $(PROJECTS) -.PHONY: doc-validator -doc-validator: ## Run doc-validator on the entire docs folder which includes pulling the latest `DOC_VALIDATOR_IMAGE` (default: `grafana/doc-validator:latest`) container image. To not pull the image, set `PULL=false`. -doc-validator: make-docs -ifeq ($(PULL), true) - $(PODMAN) pull -q $(DOC_VALIDATOR_IMAGE) -endif - DOCS_IMAGE=$(DOC_VALIDATOR_IMAGE) $(CURDIR)/make-docs $(PROJECTS) - .PHONY: vale vale: ## Run vale on the entire docs folder which includes pulling the latest `VALE_IMAGE` (default: `grafana/vale:latest`) container image. To not pull the image, set `PULL=false`. vale: make-docs diff --git a/docs/make-docs b/docs/make-docs index f6d72f905dc..d2778ecc411 100755 --- a/docs/make-docs +++ b/docs/make-docs @@ -8,6 +8,14 @@ # [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes. # Changes are relevant to this script and the support docs.mk GNU Make interface. # +# ## 9.0.0 (2025-04-05) +# +# ### Removed +# +# - doc-validator target and associated scripts. +# +# Most useful rules have been migrated to Vale and the others are often false positives. +# # ## 8.5.2 (2025-02-28) # # ### Fixed @@ -831,51 +839,6 @@ ${DOCS_IMAGE} POSIX_HERESTRING case "${image}" in - 'grafana/doc-validator') - proj="$(new_proj "$1")" - printf '\r\n' - - IFS='' read -r cmd </dev/null 2>&1; then - # shellcheck disable=SC2016 - errr '`jq` must be installed for the `doc-validator` target to work.' - # shellcheck disable=SC2016 - note 'To install `jq`, refer to https://jqlang.github.io/jq/download/,' - - exit 1 - fi - - ${cmd} \ - | jq -r '"ERROR: \(.location.path):\(.location.range.start.line // 1):\(.location.range.start.column // 1): \(.message)" + if .suggestions[0].text then "\nSuggestion: \(.suggestions[0].text)" else "" end' - ;; - json) - ${cmd} - ;; - *) # default - errr "Invalid output format '${OUTPUT_FORMAT}'" - esac - ;; 'grafana/vale') proj="$(new_proj "$1")" printf '\r\n' diff --git a/docs/sources/administration/recorded-queries/index.md b/docs/sources/administration/recorded-queries/index.md index fdeaa7da656..7460ee31249 100644 --- a/docs/sources/administration/recorded-queries/index.md +++ b/docs/sources/administration/recorded-queries/index.md @@ -15,7 +15,11 @@ title: Recorded queries weight: 300 --- -# Recorded queries +# DEPRECIATED Recorded queries + +{{% admonition type="warning" %}} +Recorded queries are deprecated. Please use the new [Grafana Managed Recording Rules](/docs/grafana/latest/alerting/alerting-rules/create-recording-rules/create-grafana-managed-recording-rules) instead. +{{% /admonition %}} Recorded queries allow you to see trends over time by taking a snapshot of a data point on a set interval. This can give you insight into historic trends. diff --git a/docs/sources/alerting/alerting-rules/alerting-migration/_index.md b/docs/sources/alerting/alerting-rules/alerting-migration/_index.md new file mode 100644 index 00000000000..50654a58f1d --- /dev/null +++ b/docs/sources/alerting/alerting-rules/alerting-migration/_index.md @@ -0,0 +1,78 @@ +--- +description: Use the Grafana Alerting import tool to convert your datasource managed alert rules into Grafana managed alert rules +labels: + products: + - cloud + - enterprise + - oss +title: Import data source-managed alert rules +menuTitle: Import to Grafana-managed alert rules +weight: 600 +refs: +--- + +# Import data source-managed alert rules + +Grafana provides an internal tool in Alerting which allows you to import Prometheus and Loki alert rules into Grafana-managed alert rules. + +## Before you begin + +The `alertingMigrationUI` and `grafanaManagedRecordingRulesDatasources` [feature flags](/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) needs to be enabled to use this feature. + +To use the migration tool, you need the following [RBAC permissions](/docs/grafana/latest/administration/roles-and-permissions/access-control/): + +- Alerting: Rules Writer +- Alerting: Set provisioning status +- Datasources: Reader +- Folders: Creator + {{< admonition type="note" >}} + The Folders permission is optional and only necessary if you want to create new folders for your target namespace. If your account doesn't have permissions to view a namespace, the tool creates a new one. It is a best practice to prepare an import plan before you convert all your alert rules. + {{< /admonition >}} + +## How it works + +When you use the import tool, a folder of data source-managed rules is copied to another folder as Grafana-managed alert rules, preserving the behavior of the rules, and the original alert rules are kept in their original location. + +When data source-managed alert rules are converted to Grafana-managed alert rules, the following are applied to the Grafana-managed alert rules: + +- All rules are given `rule_query_offset` offset value of 1m. +- The `missing_series_evals_to_resolve` is set to 1 for the new rules. +- The newly created rules are given unique UIDs. + +{{< admonition type="note" >}} +Plugin rules that have the label `__grafana_origin` are not included on alert rule imports. +{{< /admonition >}} + +## Import alert rules + +To convert data source-managed alert rules to Grafana managed alerts: + +1. Go to **Alerting > Alert rules**. + +1. Navigate to the Data source-managed alert rules section and click **Import to Grafana-managed rules**. + + The import alert rules page opens. + +1. In the Data source dropdown, select the Loki or Prometheus data source of the alert rules. + +1. In Additional settings, select a target folder or designate a new folder to import the rules into. + + If you import the rules into an existing folder, don't chose a folder with existing alert rules, as they could get overwritten. + +1. (Optional) Select a Namespace and/or Group to determine which rules are imported. + +1. (Optional) Turn on **Pause imported alerting rules**. + + Pausing stops alert rule evaluation and doesn’t create any alert instances for the newly created Grafana-managed alert rules. + +1. (Optional) Turn on **Pause imported recording rules**. + + Pausing stops alert rule evaluation behavior for the newly created Grafana-managed alert rules. + +1. Select which target data source the new recording rule is written to. + +1. Click **Import**. + + A preview shows the rules that will be imported. If your target folder contains folders with the same name of the imported folders, a warning displays to inform you. You can explore the warning to see a list of folders that might be overwritten. + + Click **Yes, import** to import the rules. diff --git a/docs/sources/alerting/alerting-rules/alerting-migration/migration-api.md b/docs/sources/alerting/alerting-rules/alerting-migration/migration-api.md new file mode 100644 index 00000000000..4a623513094 --- /dev/null +++ b/docs/sources/alerting/alerting-rules/alerting-migration/migration-api.md @@ -0,0 +1,113 @@ +--- +description: Use the Grafana Alerting API import tool to convert your datasource managed alert rules into Grafana managed alert rules +labels: + products: + - cloud + - enterprise + - oss +title: Import data source-managed alert rules with Grafana Mimirtool +menuTitle: API alert rules import +weight: 601 +refs: +--- + +# Import data source-managed alert rules with Grafana Mimirtool + +You can convert data source-managed alert rules to Grafana-managed alert rules with the Grafana import tool in the Grafana user interface, or you can convert them with the Grafana Mimirtool command-line tool. This guide tells you how to use Mimirtool to import your data source-managed alert rules. + +## Before you begin + +The `grafanaManagedRecordingRulesDatasources` [feature flag](/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) needs to be enabled to use this feature. + +To import data source-managed alert rules with Grafana Mimirtool, you need to have the Grafana Mimirtool command-line tool installed. + +You need a service account the following [RBAC permissions](/docs/grafana/latest/administration/roles-and-permissions/access-control/): + +- Alerting: Rules Reader +- Alerting: Rules Writer +- Alerting: Set provisioning status +- Datasources: Reader +- Folders: Creator +- Folders: Reader +- Folders: Writer + +You also need to create a service account token with your service account. Refer to the [documentation for more information on service accounts and service account tokens](/docs/grafana/latest/administration/service-accounts/) + +## How it works + +When data source-managed alert rules are converted to Grafana-managed alert rules, the following are applied to the Grafana-managed alert rules: + +- All rules are given `rule_query_offset` offset value of 1m. + Grafana OSS and Enterprise can configure this value in their conf: + ``` + [unified_alerting.prometheus_conversion] + rule_query_offset = 1m + ``` + If this value is set explicitly in a rule group, that value takes precedence over the configuration setting. +- The `missing_series_evals_to_resolve` is set to 1 for the new rules. +- The newly created rules are given unique UIDs. + If you don't want the UID to be automatically generated, you can specify a specific UID with the `__grafana_alert_rule_uid__` label. + +## Import alert rules with Mimirtool or coretextool + +You can use either [Mimirtool](/docs/mimir/latest/manage/tools/mimirtool/) or [`cortextool`](https://github.com/grafana/cortex-tools) (version `0.11.3` or later) to import your alert rules. For more information about Mimirtool commands, see the [Mimirtool documentation](/docs/mimir/latest/manage/tools/mimirtool/#rules). + +To convert your alert rules, use the following command prompt substituting the your URL and your service account token as indicated, followed by your intended Mimirtool command. + +```bash +MIMIR_ADDRESS=https://.grafana-dev.net/api/convert/ MIMIR_AUTH_TOKEN= MIMIR_TENANT_ID=1 +``` + +For coretextool, you need to set `--backend=loki` to import Loki alert rules. For example: + +```bash +CORTEX_ADDRESS=/api/convert/ CORTEX_AUTH_TOKEN= CORTEX_TENANT_ID=1 cortextool rules --backend=loki list +``` + +Headers can be passed to the `mimirtool` or `coretextool` via `--extra-headers`. + +For more information about the Rule API points and examples of Mimirtool commands, see the [Mimir HTTP API documentation](/docs/mimir/latest/references/http-api/#ruler-rules:~:text=config/v1/rules-,Get%20rule%20groups%20by%20namespace,DELETE%20%3Cprometheus%2Dhttp%2Dprefix%3E/config/v1/rules/%7Bnamespace%7D,-Delete%20tenant%20configuration) for more information about the Rule API points and examples of Mimirtool commands. + +{{< admonition type="note" >}} +To use the `mimirtool rules sync` command, you need to set the `--concurrency` parameter to `1` (`--concurrency=1`). The parameter defaults to 8, which may cause the API to return errors. +{{< /admonition >}} + +### Compatible endpoints + +The following are compatible API endpoints: + +**GET** + +``` +GET /convert/prometheus/config/v1/rules - Get all rule groups across all namespaces +GET /convert/prometheus/config/v1/rules/ - Get rule groups in a specific namespace +GET /convert/prometheus/config/v1/rules// - Get a single rule group + +``` + +**POST** + +``` +POST /convert/prometheus/config/v1/rules - Create/update multiple rule groups across multiple namespaces +POST /convert/prometheus/config/v1/rules/ - Create/update a single rule group in a namespace +``` + +Post rules also require the following header: +When posting rules: +`X-Grafana-Alerting-Datasource-UID` - Supply the UID of the data source to use for queries. + +**Delete** + +``` +DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} - Delete all alert rules in a namespace +DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} - Delete a specific rule group +``` + +**Optional Headers** + +Additional configuration headers for more granular import control include the following: + +- `X-Grafana-Alerting-Recording-Rules-Paused` - Set to "true" to import recording rules in paused state. +- `X-Grafana-Alerting-Alert-Rules-Paused` - Set to "true" to import alert rules in paused state. +- `X-Grafana-Alerting-Target-Datasource-UID` - Enter the UID of the target data source. +- `X-Grafana-Alerting-Folder-UID` - Enter the UID of the target destination folder for imported rules. diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 73a6621b68a..7d63d6067dd 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -236,6 +236,10 @@ To do this, you need to make sure that your alert rule is in the right evaluatio Pausing stops alert rule evaluation and doesn't create any alert instances. This is different to [mute timings](ref:mute-timings), which stop notifications from being delivered, but still allows for alert rule evaluation and the creation of alert instances. +1. Set the time threshold for alerts firing. + +You can set the minimum amount of time that an alert remains firing after the breached threshold expression no longer returns any results. This sets an alert to a "Recovering" state for the duration of time set here. The Recovering state can be used to reduce noise from flapping alerts. Select "none" stop an alert from firing immediately after the breach threshold is cleared. + 1. In **Configure no data and error handling**, you can define the alerting behavior and alerting state for two scenarios: - When the evaluation returns **No data** or all values are null. @@ -280,9 +284,13 @@ Complete the following steps to set up notifications. {{< docs/shared lookup="alerts/configure-notification-message.md" source="grafana" version="" >}} -## Restore deleted alert rules +## Permanently delete or restore deleted alert rules -Deleted alert rules are stored for 30 days. Admins can restore deleted Grafana-managed alert rules. +Only users with an Admin role can restore deleted Grafana-managed alert rules. After an alert rule is restored, it is restored with a new, different UID from the one it had before. 1. Go to **Alerts & IRM > Alerting > Recently deleted**. -1. Click the **Restore** button to restore the alert rule. +1. Click the **Restore** button to restore the alert rule or click **Delete permanently** to delete the alert rule. + +{{< admonition type="note" >}} +Deleted alert rules are stored for 30 days. Grafana Enterprise and OSS users can adjust the length of time for which the rules are stored can be adjusted in the Grafana configuration file's `[unified_alerting].deleted_rule_retention` field. For an example of how to modify the Grafana configuration file, refer to the [documentation example here](/docs/grafana/latest/alerting/set-up/configure-alert-state-history/#configuring-grafana). +{{< /admonition >}} diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md index bf4e0cc90fe..898b2ae98b5 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md @@ -33,6 +33,11 @@ refs: destination: /docs/grafana//alerting/configure-notifications/manage-contact-points/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/ + notification-templates-namespaced-functions: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/reference/#namespaced-functions + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference/#namespaced-functions --- # Configure webhook notifications @@ -70,6 +75,7 @@ For more details on contact points, including how to test them and enable notifi | Basic Authentication Password | Password for HTTP Basic Authentication. | | Authentication Header Scheme | Scheme for the `Authorization` Request Header. Default is `Bearer`. | | Authentication Header Credentials | Credentials for the `Authorization` Request header. | +| Extra Headers | Additional HTTP headers to include in the request. | | Max Alerts | Maximum number of alerts to include in a notification. Any alerts exceeding this limit are ignored. `0` means no limit. | | TLS | TLS configuration options, including CA certificate, client certificate, and client key. | | HMAC Signature | HMAC signature configuration options. | @@ -115,18 +121,11 @@ To validate incoming webhook requests from Grafana, follow these steps: Use the following settings to include custom data within the [JSON payload](#body). Both options support using [notification templates](ref:notification-templates). -| Option | Description | -| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| Title | Sends the value as a string in the `title` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | -| Message | Sends the value as a string in the `message` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | - -{{< admonition type="note" >}} -You can customize the `title` and `message` options to include custom messages and notification data using notification templates. These fields are always sent as strings in the JSON payload. - -However, you cannot customize the webhook data structure, such as adding or changing other JSON fields and HTTP headers, or sending data in a different format like XML. - -If you need to format these fields as JSON or modify other webhook request options, consider sending webhook notifications to a proxy server that adjusts the webhook request before forwarding it to the final destination. -{{< /admonition >}} +| Option | Description | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| Title | Sends the value as a string in the `title` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | +| Message | Sends the value as a string in the `message` field of the [JSON payload](#body). Supports [notification templates](ref:notification-templates). | +| [Custom Payload](#custom-payload) | Optionally override the default payload format with a custom template. | #### Optional notification settings @@ -134,7 +133,7 @@ If you need to format these fields as JSON or modify other webhook request optio | ------------------------ | ------------------------------------------------------------------- | | Disable resolved message | Enable this option to prevent notifications when an alert resolves. | -## JSON payload +## Default JSON payload The following example shows the payload of a webhook notification containing information about two firing alerts: @@ -252,3 +251,71 @@ The Alert object represents an alert included in the notification group, as prov | `dashboardURL` | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation. | | `panelURL` | string | A link to the panel if the alert has a Panel ID annotation. | | `imageURL` | string | URL of a screenshot of a panel assigned to the rule that created this notification. | + +## Custom Payload + +The `Custom Payload` option allows you to completely customize the webhook payload using templates. This gives you full control over the structure and content of the webhook request. + +| Option | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------- | +| Payload Template | Template string that defines the structure of the webhook payload. | +| Payload Variables | Key-value pairs that define additional variables available in the template under `.Vars.`. | + +Example of a custom payload template that includes variables: + +``` +{ + "alert_name": "{{ .CommonLabels.alertname }}", + "status": "{{ .Status }}", + "environment": "{{ .Vars.environment }}", + "custom_field": "{{ .Vars.custom_field }}" +} +``` + +{{< admonition type="note" >}} +When using Custom Payload, the Title and Message fields are ignored as the entire payload structure is determined by your template. +{{< /admonition >}} + +### JSON Template Functions + +When creating custom payloads, several template functions are available to help generate valid JSON structures. These include functions for creating dictionaries (`coll.Dict`), arrays (`coll.Slice`, `coll.Append`), and converting between JSON strings and objects (`data.ToJSON`, `data.JSON`). + +For detailed information about these and other template functions, refer to [notification template functions](ref:notification-templates-namespaced-functions). + +Example using JSON helper functions: + +``` +{{ define "webhook.custom.payload" -}} + {{ coll.Dict + "receiver" .Receiver + "status" .Status + "alerts" (tmpl.Exec "webhook.custom.simple_alerts" .Alerts | data.JSON) + "groupLabels" .GroupLabels + "commonLabels" .CommonLabels + "commonAnnotations" .CommonAnnotations + "externalURL" .ExternalURL + "version" "1" + "orgId" (index .Alerts 0).OrgID + "truncatedAlerts" .TruncatedAlerts + "groupKey" .GroupKey + "state" (tmpl.Inline "{{ if eq .Status \"resolved\" }}ok{{ else }}alerting{{ end }}" . ) + "allVariables" .Vars + "title" (tmpl.Exec "default.title" . ) + "message" (tmpl.Exec "default.message" . ) + | data.ToJSONPretty " "}} +{{- end }} + +{{- /* Embed json templates in other json templates. */ -}} +{{ define "webhook.custom.simple_alerts" -}} + {{- $alerts := coll.Slice -}} + {{- range . -}} + {{ $alerts = coll.Append (coll.Dict + "status" .Status + "labels" .Labels + "startsAt" .StartsAt + "endsAt" .EndsAt + ) $alerts}} + {{- end -}} + {{- $alerts | data.ToJSON -}} +{{- end }} +``` diff --git a/docs/sources/alerting/configure-notifications/template-notifications/reference.md b/docs/sources/alerting/configure-notifications/template-notifications/reference.md index 905ad86e264..665e15857c9 100644 --- a/docs/sources/alerting/configure-notifications/template-notifications/reference.md +++ b/docs/sources/alerting/configure-notifications/template-notifications/reference.md @@ -56,17 +56,19 @@ This documentation lists the data available for use in notification templates. In notification templates, dot (`.`) is initialized with the following data: -| Name | Type | Description | -| ------------------- | ----------------- | ----------------------------------------------------------------------------------------------------- | -| `Receiver` | string | The name of the contact point sending the notification | -| `Status` | string | The status is `firing` if at least one alert is firing, otherwise `resolved`. | -| `Alerts` | [][Alert](#alert) | List of all firing and resolved alerts in this notification. | -| `Alerts.Firing` | [][Alert](#alert) | List of all firing alerts in this notification. | -| `Alerts.Resolved` | [][Alert](#alert) | List of all resolved alerts in this notification. | -| `GroupLabels` | [KV](#kv) | The labels that group these alerts in this notification based on the `Group by` option. | -| `CommonLabels` | [KV](#kv) | The labels common to all alerts in this notification. | -| `CommonAnnotations` | [KV](#kv) | The annotations common to all alerts in this notification. | -| `ExternalURL` | string | A link to Grafana, or the Alertmanager that sent this notification if using an external Alertmanager. | +| Name | Type | Description | +| ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- | +| `Receiver` | string | The name of the contact point sending the notification | +| `Status` | string | The status is `firing` if at least one alert is firing, otherwise `resolved`. | +| `Alerts` | [][Alert](#alert) | List of all firing and resolved alerts in this notification. | +| `Alerts.Firing` | [][Alert](#alert) | List of all firing alerts in this notification. | +| `Alerts.Resolved` | [][Alert](#alert) | List of all resolved alerts in this notification. | +| `GroupLabels` | [KV](#kv) | The labels that group these alerts in this notification based on the `Group by` option. | +| `CommonLabels` | [KV](#kv) | The labels common to all alerts in this notification. | +| `CommonAnnotations` | [KV](#kv) | The annotations common to all alerts in this notification. | +| `ExternalURL` | string | A link to Grafana, or the Alertmanager that sent this notification if using an external Alertmanager. | +| `GroupKey` | string | The key used to identify this alert group. | +| `TruncatedAlerts` | integer | The number of alerts, if any, that were truncated in the notification. Supported by Webhook and OnCall. | It's important to remember that [a single notification can group multiple alerts](ref:alert-grouping) to reduce the number of alerts you receive. `Alerts` is an array that includes all the alerts in the notification. @@ -115,6 +117,7 @@ Grafana-managed alerts include these additional properties: | `SilenceURL` | string | A link to silence the alert. | | `Values` | [KV](#kv) | The values of expressions used to evaluate the alert condition. Only relevant values are included. | | `ValueString` | string | A string that contains the labels and value of each reduced expression in the alert. | +| `OrgID` | integer | The ID of the organization that owns the alert. | This example iterates over the list of firing and resolved alerts (`.Alerts`) in the notification and prints the data for each alert: @@ -264,6 +267,176 @@ You can then use `tz` to change the timezone from UTC to local time, such as `Eu 21:01:45 CET ``` +## Namespaced Functions + +In addition to the top-level functions, the following namespaced functions are also available: + +### Collection Functions + +| Name | Arguments | Returns | Description | +| ------------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `coll.Dict` | key string, value any, ... | map | Creates a map with string keys from key/value pairs. All keys are converted to strings. If an odd number of arguments is provided, the last key will have an empty string value. | +| `coll.Slice` | ...any | []any | Creates a slice (array/list) from the provided arguments. Useful for creating lists that can be iterated over with `range`. | +| `coll.Append` | value any, list []any | []any | Creates a new list by appending a value to the end of an existing list. Does not modify the original list. | + +Example using collection functions: + +```go +{{ define "collection.example" }} +{{- /* Create a dictionary of alert metadata */ -}} +{{- $metadata := coll.Dict + "severity" "critical" + "team" "infrastructure" + "environment" "production" +-}} + +{{- /* Create a slice of affected services */ -}} +{{- $services := coll.Slice "database" "cache" "api" -}} + +{{- /* Append a new service to the list */ -}} +{{- $services = coll.Append "web" $services -}} + +{{- /* Use the collections in a template */ -}} +Affected Services: {{ range $services }}{{ . }},{{ end }} + +Alert Metadata: +{{- range $k, $v := $metadata }} + {{ $k }}: {{ $v }} +{{- end }} +{{ end }} +``` + +Output: + +``` +Affected Services: database,cache,api,web, + +Alert Metadata: + environment: production + severity: critical + team: infrastructure +``` + +### Data Functions + +| Name | Arguments | Returns | Description | +| ------------------- | ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `data.JSON` | jsonString string | any | Parses a JSON string into an object that can be manipulated in the template. Works with both JSON objects and arrays. | +| `data.ToJSON` | obj any | string | Serializes any object (maps, arrays, etc.) into a JSON string. Useful for creating webhook payloads. | +| `data.ToJSONPretty` | indent string, obj any | string | Creates an indented JSON string representation of an object. The first argument specifies the indentation string (e.g., spaces). | + +Example using data functions: + +```go +{{ define "data.example" }} +{{- /* First, let's create some alert data as a JSON string */ -}} +{{ $jsonString := `{ + "service": { + "name": "payment-api", + "environment": "production", + "thresholds": { + "error_rate": 5, + "latency_ms": 100 + } + } +}` }} + +{{- /* Parse the JSON string into an object we can work with */ -}} +{{ $config := $jsonString | data.JSON }} + +{{- /* Create a new alert payload */ -}} +{{ $payload := coll.Dict + "service" $config.service.name + "environment" $config.service.environment + "status" .Status + "errorThreshold" $config.service.thresholds.error_rate +}} + +{{- /* Output the payload in different JSON formats */ -}} +Compact JSON: {{ $payload | data.ToJSON }} + +Pretty JSON with 2-space indent: +{{ $payload | data.ToJSONPretty " " }} +{{ end }} +``` + +Output: + +``` +Compact JSON: {"environment":"production","errorThreshold":5,"service":"payment-api","status":"resolved"} + +Pretty JSON with 2-space indent: +{ + "environment": "production", + "errorThreshold": 5, + "service": "payment-api", + "status": "resolved" +} +``` + +### Template Functions + +| Name | Arguments | Returns | Description | +| ------------- | ---------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `tmpl.Exec` | name string, [context any] | string | Executes a named template and returns the result as a string. Similar to the `template` action but allows for post-processing of the result. | +| `tmpl.Inline` | template string, context any | string | Renders a string as a template. | + +```go +{{ define "template.example" -}} +{{ coll.Dict + "info" (tmpl.Exec `info` . | data.JSON) + "severity" (tmpl.Inline `{{ print "critical" | toUpper }}` . ) + | data.ToJSONPretty " "}} +{{- end }} + +{{- /* Define a sub-template */ -}} +{{ define "info" -}} +{{coll.Dict + "team" "infrastructure" + "environment" "production" | data.ToJSON }} +{{- end }} +``` + +Output: + +```json +{ + "info": { + "environment": "production", + "team": "infrastructure" + }, + "severity": "CRITICAL" +} +``` + +### Time Functions + +| Name | Arguments | Returns | Description | +| ---------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------ | +| `time.Now` | | Time | Returns the current local time as a time.Time object. Can be formatted using Go's time formatting functions. | + +Example using time functions: + +```go +{{ define "time.example" }} +{{- /* Get current time in different formats */ -}} +Current Time (UTC): {{ (time.Now).UTC.Format "2006-01-02 15:04:05 MST" }} +Current Time (Local): {{ (time.Now).Format "Monday, January 2, 2006 at 15:04:05" }} + +{{- /* Compare alert time with current time */ -}} +{{ $timeAgo := (time.Now).Sub .StartsAt }} +Alert fired: {{ $timeAgo }} ago +{{ end }} +``` + +Output: + +``` +Current Time (UTC): 2025-03-08 18:14:27 UTC +Current Time (Local): Saturday, March 8, 2025 at 14:14:27 +Alert fired: 25h49m32.78574723s ago +``` + ## Differences with annotation and label templates In the alert rule, you can also template annotations and labels to include additional information. For example, you might add a `summary` annotation that displays the query value triggering the alert. diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md index 42b3faa03ee..bc610c4457f 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md @@ -29,7 +29,7 @@ The criteria determining when an alert rule fires are based on two settings: - [Evaluation group](#evaluation-group): how frequently the alert rule is evaluated. - [Pending period](#pending-period): how long the condition must be met to start firing. -{{< figure src="/media/docs/alerting/alert-rule-evaluation.png" max-width="750px" alt="Set the evaluation behavior of the alert rule in Grafana." caption="Set alert rule evaluation" >}} +{{< figure src="/media/docs/alerting/alert-rule-evaluation-2.png" max-width="750px" alt="Set the evaluation behavior of the alert rule in Grafana." caption="Set alert rule evaluation" >}} ## Evaluation group @@ -53,6 +53,12 @@ The pending period specifies how long the condition must be met before firing, e You can also set the pending period to zero to skip it and have the alert fire immediately once the condition is met. +## Keep firing for + +You can set a period to keep an alert firing after the threshold is no longer breached. This sets the alert to a Recovering state. In a Recovering state, the alert won’t fire again if the threshold is breached. The Keep firing timer is then reset and the alert transitions back to Alerting state. + +The Keep firing for period helps reduce repeated firing-resolving-firing notification scenarios caused by flapping alerts. + ## Evaluation example Keep in mind: diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md index 3cfbc85c31e..b7987e29828 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md @@ -29,6 +29,11 @@ refs: destination: /docs/grafana//alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-grafana-managed-rule/#configure-no-data-and-error-handling + keep-firing: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/alert-rule-evaluation/#keep-firing-for + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rule-evaluation/#keep-firing-for notifications: - pattern: /docs/grafana/ destination: /docs/grafana//alerting/fundamentals/notifications/ @@ -49,12 +54,13 @@ An alert instance can be in either of the following states: | **Normal** | The state of an alert when the condition (threshold) is not met. | | **Pending** | The state of an alert that has breached the threshold but for less than the [pending period](ref:pending-period). | | **Alerting** | The state of an alert that has breached the threshold for longer than the [pending period](ref:pending-period). | +| **Recovering** | The state of an alert that has been configured to keep [firing for a duration after it is triggered](ref:keep-firing). | | **No Data\*** | The state of an alert whose query returns no data or all values are null.
An alert in this state generates a new [DatasourceNoData alert](#no-data-and-error-alerts). You can [modify the default behavior of the no data state](#modify-the-no-data-or-error-state). | | **Error\*** | The state of an alert when an error or timeout occurred evaluating the alert rule.
An alert in this state generates a new [DatasourceError alert](#no-data-and-error-alerts). You can [modify the default behavior of the error state](#modify-the-no-data-or-error-state). | If an alert rule changes (except for updates to annotations, the evaluation interval, or other internal fields), its alert instances reset to the `Normal` state. The alert instance state then updates accordingly during the next evaluation. -{{< figure src="/media/docs/alerting/alert-instance-states-v3.png" caption="Alert instance state diagram" alt="A diagram of the distinct alert instance states and transitions." max-width="750px" >}} +{{< figure src="/media/docs/alerting/alert-state-diagram2.png" caption="Alert instance state diagram" alt="A diagram of the distinct alert instance states and transitions." max-width="750px" >}} {{< admonition type="note" >}} diff --git a/docs/sources/alerting/monitor-status/view-alert-rules.md b/docs/sources/alerting/monitor-status/view-alert-rules.md index f521800f5bb..199941c9578 100644 --- a/docs/sources/alerting/monitor-status/view-alert-rules.md +++ b/docs/sources/alerting/monitor-status/view-alert-rules.md @@ -61,7 +61,11 @@ For details on how rule states and alert instance states are displayed, refer to ## View, compare and restore alert rules versions. -You can view, compare, and restore previous alert rule versions. The number of alert rule versions is limited to a maximum of 10 alert rule versions for free users, and a maximum of 100 stored alert rule versions for paid tier users. +You can view, compare, and restore previous alert rule versions. + +{{< admonition type="note" >}} +In Grafana OSS and Enterprise, the number of alert rule versions is limited. Free users are allowed a maximum of 10 alert rule versions, while paid users have a maximum of 100 stored alert rule versions. +{{< /admonition >}} To view or restore previous versions for an alert rule, complete the following steps. diff --git a/docs/sources/dashboards/_index.md b/docs/sources/dashboards/_index.md index 69ea55c50fb..c24b99d34cc 100644 --- a/docs/sources/dashboards/_index.md +++ b/docs/sources/dashboards/_index.md @@ -43,6 +43,12 @@ cards: href: ./share-dashboards-panels/shared-dashboards/ description: Share your dashboards with anyone without requiring access to your Grafana organization. height: 24 +refs: + panels: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/panel-overview/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-overview/ --- {{< docs/hero-simple key="hero" >}} @@ -51,13 +57,13 @@ cards: ## Overview -
+{{< shared id="dashboard-overview" >}} -A Grafana dashboard is a set of one or more [panels](/docs/grafana//panels-visualizations/panel-overview/), organized and arranged into one or more rows, that provide an at-a-glance view of related information. These panels are created using components that query and transform raw data from a data source into charts, graphs, and other visualizations. +A Grafana dashboard is a set of one or more [panels](ref:panels), organized and arranged into one or more rows, that provide an at-a-glance view of related information. These panels are created using components that query and transform raw data from a data source into charts, graphs, and other visualizations. A data source can be an SQL database, Grafana Loki, Grafana Mimir, or a JSON-based API. It can even be a basic CSV file. Data source plugins take a query you want answered, retrieve the data from the data source, and reconcile the differences between the data model of the data source and the data model of Grafana dashboards. -
+{{< /shared >}} Queries allow you to reduce the entirety of your data to a specific dataset, providing a more manageable visualization. Since data sources have their own distinct query languages, Grafana dashboards provide you with a query editor to accommodate these differences. diff --git a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md index d2a40c6fd6b..033972b0b12 100644 --- a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md +++ b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md @@ -90,7 +90,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee **To create a dashboard**: -
+{{< shared id="create-dashboard" >}} 1. Click **Dashboards** in the main menu. 1. Click **New** and select **New Dashboard**. @@ -98,7 +98,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee ![Empty dashboard state](/media/docs/grafana/dashboards/empty-dashboard-10.2.png) -
+{{< /shared >}} 1. In the dialog box that opens, do one of the following: diff --git a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md b/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md index eacc91861df..cd34e8501d0 100644 --- a/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md +++ b/docs/sources/datasources/elasticsearch/configure-elasticsearch-data-source.md @@ -127,7 +127,7 @@ Additional settings are optional settings that can be configured for more contro The following settings are specific to the Elasticsearch data source. -- **Index name** - Use the index settings to specify a default for the `time field` and your Elasticsearch index's name. You can use a time pattern, such as `YYYY.MM.DD`, or a wildcard for the index name. +- **Index name** - Use the index settings to specify a default for the `time field` and your Elasticsearch index's name. You can use a time pattern, for example `[logstash-]YYYY.MM.DD`, or a wildcard for the index name. When specifying a time pattern, the fixed part(s) of the pattern should be wrapped in square brackets. - **Pattern** - Select the matching pattern if using one in your index name. Options include: @@ -138,6 +138,8 @@ The following settings are specific to the Elasticsearch data source. - monthly - yearly +Only select a pattern option if you have specified a time pattern in the Index name field. + - **Time field name** - Name of the time field. The default value is @timestamp. You can enter a different name. - **Max concurrent shard requests** - Sets the number of shards being queried at the same time. The default is `5`. For more information on shards see [Elasticsearch's documentation](https://www.elastic.co/guide/en/elasticsearch/reference/8.9/scalability.html#scalability). diff --git a/docs/sources/panels-visualizations/visualizations/annotations/index.md b/docs/sources/panels-visualizations/visualizations/annotations/index.md index 673636227ae..d69ea3d95de 100644 --- a/docs/sources/panels-visualizations/visualizations/annotations/index.md +++ b/docs/sources/panels-visualizations/visualizations/annotations/index.md @@ -22,69 +22,71 @@ weight: 100 The annotations list shows a list of available annotations you can use to view annotated data. Various options are available to filter the list based on tags and on the current dashboard. -## Panel options +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-annotations-list-viz-v12.0.png" max-width="750px" alt="The annotations list visualization" >}} + +## Configuration options + +{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} + +### Panel options {{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}} -## Annotation query +### Annotation query options -The following options control the source query for the list of annotations. +The following options control the source query for the list of annotations: -### Query Filter + -Use the query filter to create a list of annotations from all dashboards in your organization or the current dashboard in which this panel is located. It has the following options: +| Option | Description | +| ---------- | --------------------------------------------------------------------------------------------------------- | +| [Query filter](#query-filter) | Specify which annotations are included in the list. | +| [Time Range](#time-range) | Specify whether the list should be limited to the current time range. | +| Tags | Filter the annotations by tags. You can add multiple tags to refine the list. Optionally, leave the tag list empty and filter in view mode by selecting tags that are listed as part of the results on the panel itself. | +| Limit | Limit the number of results returned. | -- All dashboards - List annotations from all dashboards in the current organization. -- This dashboard - Limit the list to the annotations on the current dashboard. + -### Time Range +#### Query filter -Use the time range option to specify whether the list should be limited to the current time range. It has the following options: +Use the **Query filter** option to create a list of annotations from all dashboards in your organization or the current dashboard in which this panel is located. +Choose from: -- None - no time range limit for the annotations query. -- This dashboard - Limit the list to the time range of the dashboard where the annotations list is available. +- **All dashboards** - List annotations from all dashboards in the current organization. +- **This dashboard** - Limit the list to the annotations on the current dashboard. -### Tags +#### Time Range -Use the tags option to filter the annotations by tags. You can add multiple tags in order to refine the list. +Specify whether the list should be limited to the current time range. +Choose from: -{{% admonition type="note" %}} -Optionally, leave the tag list empty and filter on the fly by selecting tags that are listed as part of the results on the panel itself. -{{% /admonition %}} +- **None** - No time range limit for the annotations query. +- **This dashboard** - Limit the list to the time range of the dashboard where the annotations list is available. -### Limit +### Display options -Use the limit option to limit the number of results returned. +These options control additional metadata included in the annotations list display: -## Display + -These options control additional meta-data included in the annotations list display. +| Option | Description | +| ---------- | --------------------------------------------------------------------------------------------------------- | +| Show user | Show or hide which user created the annotation. | +| Show time | Show or hide the time the annotation creation time. | +| Show tags | Show or hide the tags associated with an annotation. Note that you can use the tags to filter the annotations list. | -### Show user + -Use this option to show or hide which user created the annotation. +### Link behavior options -### Show time +Use the following options to control the behavior of annotation links in the list: -Use this option to show or hide the time the annotation creation time. + -### Show Tags +| Option | Description | +| ---------- | --------------------------------------------------------------------------------------------------------- | +| Link target | Set how to view the annotated data. Choose from:
  • **Panel** - The link takes you directly to a full-screen view of the panel with the corresponding annotation.
  • **Dashboard** - Focuses the annotation in the context of a complete dashboard.
| +| Time before | Set the time range before the annotation. Use duration string values like `1h` for one hour and `10m` for 10 minutes. | +| Time after | Set the time range after the annotation. | -Use this option to show or hide the tags associated with an annotation. _NB_: You can use the tags to live-filter the annotations list on the visualization itself. - -## Link behavior - -### Link target - -Use this option to chose how to view the annotated data. It has the following options. - -- Panel - This option will take you directly to a full-screen view of the panel with the corresponding annotation -- Dashboard - This option will focus the annotation in the context of a complete dashboard - -### Time before - -Use this option to set the time range before the annotation. Use duration string values like "1h" = 1 hour, "10m" = 10 minutes, etc. - -### Time after - -Use this option to set the time range after the annotation. + diff --git a/docs/sources/panels-visualizations/visualizations/datagrid/index.md b/docs/sources/panels-visualizations/visualizations/datagrid/index.md index 4d32a93d20f..60d9aab5201 100644 --- a/docs/sources/panels-visualizations/visualizations/datagrid/index.md +++ b/docs/sources/panels-visualizations/visualizations/datagrid/index.md @@ -29,23 +29,20 @@ refs: # Datagrid -{{% admonition type="note" %}} - -The Grafana datagrid is experimental. This feature is supported by the engineering team on a best-effort basis, and breaking changes may occur without notice prior to general availability. - -{{% /admonition %}} +{{< docs/experimental product="The datagrid visualization" featureFlag="`enableDatagridEditing`" >}} Datagrids offer you the ability to create, edit, and fine-tune data within Grafana. As such, this panel can act as a data source for other panels inside a dashboard. -![Datagrid panel](/media/docs/datagrid/screenshot-grafana-datagrid-panel.png) +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-datagrid-visualization-v12.0.png" max-width="750px" alt="The datagrid visualization" >}} -Through it, you can manipulate data queried from any data source, you can start from a blank slate, or you can pull data from a dragged and dropped file. You can then use the panel as a simple tabular -visualization, or you can modify the data—and even remove it altogether—to create a blank slate. +Through it, you can manipulate data queried from any data source, you can start from a blank slate, or you can pull data from a dragged and dropped file. +You can then use the panel as a simple tabular visualization, or you can modify the data—and even remove it altogether—to create a blank slate. Editing the dataset changes the data source to use the inbuilt `-- Grafana --` data source, thus replacing the old data source settings and related queries, while also copying the current dataset into the dashboard model. -You can then use the panel as a data source for other panels, by using the inbuilt `-- Dashboard --` data source to pull the datagrid data. This allows for an interactive dashboard experience, where you can modify the data and see the changes reflected in other panels. +You can then use the panel as a data source for other panels, by using the inbuilt `-- Dashboard --` data source to pull the datagrid data. +This allows for an interactive dashboard experience, where you can modify the data and see the changes reflected in other panels. Learn more about the inbuilt `-- Grafana --` and `-- Dashboard --` data sources in the [special data sources](ref:special-data-sources) documentation. @@ -65,13 +62,9 @@ Deleting a row or column will remove the data from the datagrid, while clearing You can also access a header menu by clicking the dropdown icon next to the header title. From here, you can not only delete or clear a column, but also rename it, freeze it, or convert the field type of the column. -{{< figure src="/media/docs/datagrid/screenshot-grafana-datagrid-header-menu-2.png" alt="Datagrid header menu" max-width="500px" >}} +{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-datagrid-header-menu-v12.0.png" alt="Datagrid header menu" max-width="400px" >}} -## Selecting series - -If there are multiple series, you can set the datagrid to display the preferred dataset using the **Select series** dropdown in the panel options. - -## Using datagrids +## Use datagrids Datagrids offer various ways of interacting with your data. You can add, edit, move, clear, and remove rows and columns; use the inbuilt search functionality to find specific data; and convert field types or freeze horizontal scroll on a specific column. @@ -79,7 +72,7 @@ Datagrids offer various ways of interacting with your data. You can add, edit, m You can add data to a datagrid by creating a new column or row. -To create a new column, take the following steps: +To create a new column, follow these steps: 1. In an existing panel, click the **+** button in the table header after the last column. 1. When prompted, add a name for the new column. @@ -91,7 +84,7 @@ To add a new row, click a **+** button after the last row. The button is present ### Edit data -You can edit data by taking the following steps: +To edit data, follow these steps: 1. Double-click on the cell that needs to be modified. This will activate the cell and allow you to edit the data. 1. After editing the data, click anywhere outside the cell or press the Enter key to finalize the edit. @@ -102,7 +95,7 @@ To easily clear a cell of data, you can click on a cell to focus it and then pre You can move columns and rows as needed. -To move a column, take the following steps: +To move a column, follow these steps: 1. Click and hold the header of the column that needs to be moved. 1. Drag the column to the desired location. @@ -116,7 +109,7 @@ You can select multiple cells by clicking on a single cell and dragging the mous ### Delete/clear multiple rows or columns -To delete or clear multiple rows, take the following steps: +To delete or clear multiple rows, follow these steps: 1. Hover over the number column (to the left of the first column in the grid) to display row checkbox. 1. Select the checkboxes for the rows you want to work with. @@ -126,8 +119,16 @@ To delete or clear multiple rows, take the following steps: The same rules apply to columns by clicking the column headers. -To delete all rows, use the "select all" checkbox at the top left corner of the datagrid. This selects all rows and allows you to delete them using the context menu. +To delete all rows, use the select all checkbox at the top left corner of the datagrid. This selects all rows and allows you to delete them using the context menu. -## Panel options +## Configuration options + +{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} + +### Panel options {{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}} + +### Datagrid options + +If there are multiple series, you can choose the dataset the datagrid displays using the **Select series** option. diff --git a/docs/sources/panels-visualizations/visualizations/news/index.md b/docs/sources/panels-visualizations/visualizations/news/index.md index 8cb516606e6..5ceaa44605c 100644 --- a/docs/sources/panels-visualizations/visualizations/news/index.md +++ b/docs/sources/panels-visualizations/visualizations/news/index.md @@ -34,7 +34,7 @@ You can use the news visualization to provide regular news and updates to your u ## Configure a news visualization -Once you’ve created a [dashboard](https://grafana.com/docs/grafana//dashboards/build-dashboards/create-dashboard/), enter the URL of an RSS in the [URL](#url) field in the **News** section. This visualization type doesn't accept any other queries, and you shouldn't expect to be able to filter or query the RSS feed data in any way using this visualization. +After you’ve created a [dashboard](https://grafana.com/docs/grafana//dashboards/build-dashboards/create-dashboard/), enter the URL of an RSS in the **URL** field in the **News** section. This visualization type doesn't accept any other queries, and you shouldn't expect to be able to filter or query the RSS feed data in any way using this visualization. If you're having trouble loading an RSS feed, you can try rehosting the feed on a different server or using a CORS proxy. A CORS proxy is a tool that allows you to bypass CORS restrictions by making requests to the RSS feed on your behalf. You can find more information about using CORS proxies online. @@ -44,18 +44,17 @@ If you're unable to display an RSS feed using the news visualization, you can tr The news visualization supports RSS and Atom feeds. -## Panel options +## Configuration options + +{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} + +### Panel options {{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}} -## News options +### News options -Use the following options to refine your news visualization. +Use the following options to refine your news visualization: -### URL - -The URL of the RSS or Atom feed. - -### Show image - -Controls if the news social image is displayed beside the text content. +- **URL** - The URL of the RSS or Atom feed. +- **Show image** - Controls if the news social image is displayed beside the text content. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index d7f0c50cc8a..ad403d30d28 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2536,7 +2536,8 @@ Address string of selected the high availability (HA) Live engine. For Redis, it ```ini [live] ha_engine = redis -ha_engine_address = 127.0.0.1:6379 +ha_engine_address: redis-headless.grafana.svc.cluster.local:6379 +ha_engine_password: $__file{/your/redis/password/secret/mount} ```
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 51df92d1ac0..0eeb066d7b7 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -32,7 +32,6 @@ 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 | -| `prometheusRunQueriesInParallel` | Enables running Prometheus queries in parallel | Yes | | `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | | `unifiedRequestLog` | Writes error logs to the request logger | Yes | | `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | Yes | @@ -47,6 +46,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | | `formatString` | Enable format string transformer | Yes | | `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | Yes | +| `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s | Yes | | `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes | | `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes | | `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes | @@ -56,7 +56,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | | `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes | | `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | -| `exploreMetrics` | Enables the new Grafana Metrics Drilldown core app | Yes | | `alertingSimplifiedRouting` | Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule | Yes | | `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | | `lokiQueryHints` | Enables query hints for Loki | Yes | @@ -86,7 +85,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | | `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes | | `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes | -| `exploreMetricsUseExternalAppPlugin` | Use the externalized Grafana Metrics Drilldown (formerly known as Explore Metrics) app plugin | Yes | | `unifiedNavbars` | Enables unified navbars | | ## Public preview feature toggles @@ -118,103 +116,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `azureMonitorLogsBuilderEditor` | Enables the logs builder mode for the Azure Monitor data source | | `logsPanelControls` | Enables a control component for the logs panel in Explore | -## Experimental feature toggles - -[Experimental](https://grafana.com/docs/release-life-cycle/#experimental) features are early in their development lifecycle and so are not yet supported in Grafana Cloud. -Experimental features might be changed or removed without prior notice. - -| Feature toggle name | Description | -| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | -| `storage` | Configurable storage for dashboards, datasources, and resources | -| `canvasPanelNesting` | Allow elements nesting | -| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | -| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | -| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | -| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | -| `alertingBacktesting` | Rule backtesting API for alerting | -| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiShardSplitting` | Use stream shards to split queries into smaller subqueries | -| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | -| `individualCookiePreferences` | Support overriding cookie preferences per user | -| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | -| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | -| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | -| `extraThemes` | Enables extra themes | -| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `datasourceAPIServers` | Expose some datasources as apiservers. | -| `provisioning` | Next generation provisioning... and git | -| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | -| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | -| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | -| `libraryPanelRBAC` | Enables RBAC support for library panels | -| `wargamesTesting` | Placeholder feature flag for internal testing | -| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | -| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | -| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | -| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | -| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | -| `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s | -| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | -| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | -| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | -| `queryServiceFromUI` | Routes requests to the new query service | -| `queryServiceFromExplore` | Routes explore requests to the new query service | -| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | -| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | -| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | -| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | -| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | -| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | -| `dashboardNewLayouts` | Enables experimental new dashboard layouts | -| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | -| `tableSharedCrosshair` | Enables shared crosshair in table panel | -| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | -| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | -| `secretsManagementAppPlatform` | Enable the secrets management API and services under app platform | -| `scopeApi` | In-development feature flag for the scope api using the app platform. | -| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | -| `expressionParser` | Enable new expression parser | -| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | -| `queryLibrary` | Enables Query Library feature in Explore | -| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | -| `alertingListViewV2` | Enables the new alert list view design | -| `alertingCentralAlertHistory` | Enables the new central alert history. | -| `dataplaneAggregator` | Enable grafana dataplane aggregator | -| `tableNextGen` | Allows access to the new react-data-grid based table component. | -| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | -| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | -| `exploreLogsShardSplitting` | Used in Logs Drilldown to split queries into multiple queries based on the number of shards | -| `exploreLogsAggregatedMetrics` | Used in Logs Drilldown to query by aggregated metrics | -| `exploreLogsLimitedTimeRange` | Used in Logs Drilldown to limit the time range | -| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | -| `rolePickerDrawer` | Enables the new role picker drawer design | -| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | -| `timeRangeProvider` | Enables time pickers sync | -| `playlistsReconciler` | Enables experimental reconciler for playlists | -| `exploreMetricsRelatedLogs` | Display Related Logs in Grafana Metrics Drilldown | -| `prometheusSpecialCharsInLabelValues` | Adds support for quotes and special characters in label values for Prometheus queries | -| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | -| `enableSCIM` | Enables SCIM support for user and group management | -| `crashDetection` | Enables browser crash detection reporting to Faro. | -| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | -| `unifiedHistory` | Displays the navigation history so the user can navigate back to previous pages | -| `investigationsBackend` | Enable the investigations backend API | -| `k8SFolderCounts` | Enable folder's api server counts | -| `k8SFolderMove` | Enable folder's api server move | -| `templateVariablesUsesCombobox` | Use new **Combobox** component for template variables | -| `grafanaAdvisor` | Enables Advisor app | -| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | -| `newLogsPanel` | Enables the new logs panel in Explore | -| `pluginsCDNSyncLoader` | Loads plugins from CDN synchronously | -| `assetSriChecks` | Enables SRI checks for Grafana JavaScript assets | -| `localeFormatPreference` | Specifies the locale so the correct format for numbers and dates can be shown | -| `extensionSidebar` | Enables the extension sidebar | -| `localizationForPlugins` | Enables localization for plugins | -| `metricsFromProfiles` | Enables creating metrics from profiles and storing them as recording rules | - ## Development feature toggles The following toggles require explicitly setting Grafana's [app mode](../#app_mode) to 'development' before you can enable this feature toggle. These features tend to be experimental. diff --git a/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md b/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md index 760079cf103..fa4fdc74b9f 100644 --- a/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-scim-provisioning/_index.md @@ -44,20 +44,29 @@ SCIM offers several advantages for managing users and teams in Grafana: - **Reduced administrative overhead**: Eliminate manual user management tasks and reduce the risk of human error - **Enhanced security**: Automatically disable access when users leave your organization -## Identity provider consistency +## Authentication and access requirements -Grafana follows the best practice of not mixing different identity providers and SSO methods. When you enable SCIM in Grafana, you must use the same identity provider for both authentication and user provisioning. This means that users attempting to log in through other authentication methods like LDAP or OAuth will be blocked from accessing Grafana. +When you enable SCIM in Grafana, the following requirements and restrictions apply: -Users with Basic Auth credentials and those using their grafana.com accounts will still be able to log in successfully. +1. **Use the same identity provider**: You must use the same identity provider for both authentication and user provisioning. For example, if you use Azure AD for SCIM, you must also use Azure AD for authentication. + +2. **Authentication restrictions**: + + - Users attempting to log in through other methods (LDAP, OAuth) will be blocked + - By default, users who are not provisioned through SCIM cannot access Grafana + - You can allow non-SCIM users by setting `allow_non_provisioned_users = true` + +3. **Exceptions**: Users with Basic Auth credentials and those using their Grafana Cloud accounts can still log in regardless of these restrictions. ## Configure SCIM in Grafana The table below describes all SCIM configuration options. Like any other Grafana configuration, you can apply these options as [environment variables](/docs/grafana//setup-grafana/configure-grafana/#override-configuration-with-environment-variables). -| Setting | Required | Description | Default | -| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | -| `user_sync_enabled` | Yes | Enable SCIM user provisioning. When enabled, Grafana will create, update, and deactivate users based on SCIM requests from your identity provider. | `true` | -| `group_sync_enabled` | No | Enable SCIM group provisioning. When enabled, Grafana will create, update, and delete teams based on SCIM requests from your identity provider. Cannot be enabled if Team Sync is enabled. | `false` | +| Setting | Required | Description | Default | +| ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `user_sync_enabled` | Yes | Enable SCIM user provisioning. When enabled, Grafana will create, update, and deactivate users based on SCIM requests from your identity provider. | `false` | +| `group_sync_enabled` | No | Enable SCIM group provisioning. When enabled, Grafana will create, update, and delete teams based on SCIM requests from your identity provider. Cannot be enabled if Team Sync is enabled. | `false` | +| `allow_non_provisioned_users` | No | Allow non SCIM provisioned users to sign in to Grafana. | `false` | {{< admonition type="warning" >}} **Team Sync Compatibility**: diff --git a/docs/sources/setup-grafana/set-up-grafana-live.md b/docs/sources/setup-grafana/set-up-grafana-live.md index b2d1c605384..fc39dbff899 100644 --- a/docs/sources/setup-grafana/set-up-grafana-live.md +++ b/docs/sources/setup-grafana/set-up-grafana-live.md @@ -223,26 +223,14 @@ After running: - Streaming from Telegraf delivers messages to all subscribers. - A separate unidirectional stream between Grafana and backend data source opens on different Grafana servers. Publishing data to a channel delivers messages to instance subscribers, as a result, publications from different instances on different machines do not produce duplicate data on panels. -At the moment we only support single Redis node. +{{< admonition type="note" >}} +Live currently does not support Redis Sentinel. We recommend using a Redis Cluster for high-availability via a k8s helm chart such as the Bitnami Redis chart which has values to provision a Redis Cluster. Grafana Live can then be pointed to the `redis-headless` service. -> **Note:** It's possible to use Redis Sentinel and Haproxy to achieve a highly available Redis setup. Redis nodes should be managed by [Redis Sentinel](https://redis.io/topics/sentinel) to achieve automatic failover. Haproxy configuration example: -> -> ``` -> listen redis -> server redis-01 127.0.0.1:6380 check port 6380 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 on-marked-down shutdown-sessions on-marked-up shutdown-backup-sessions -> server redis-02 127.0.0.1:6381 check port 6381 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 backup -> bind *:6379 -> mode tcp -> option tcpka -> option tcplog -> option tcp-check -> tcp-check send PING\r\n -> tcp-check expect string +PONG -> tcp-check send info\ replication\r\n -> tcp-check expect string role:master -> tcp-check send QUIT\r\n -> tcp-check expect string +OK -> balance roundrobin -> ``` -> -> Next, point Grafana Live to Haproxy address:port. +``` + live: + ha_engine: redis + ha_engine_address: redis-headless.grafana.svc.cluster.local:6379 + ha_engine_password: $__file{/your/redis/password/secret/mount} +``` + +{{< /admonition >}} diff --git a/go.mod b/go.mod index c8bca87d601..e77ec0aee7a 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,7 @@ require ( github.com/googleapis/go-sql-spanner v1.11.1 // @grafana/grafana-search-and-storage github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics @@ -94,7 +94,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.2.1 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.275.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling @@ -141,7 +141,7 @@ require ( github.com/prometheus/alertmanager v0.27.0 // @grafana/alerting-backend github.com/prometheus/client_golang v1.21.1 // @grafana/alerting-backend github.com/prometheus/client_model v0.6.1 // @grafana/grafana-backend-group - github.com/prometheus/common v0.62.0 // @grafana/alerting-backend + github.com/prometheus/common v0.63.0 // @grafana/alerting-backend github.com/prometheus/prometheus v0.301.0 // @grafana/alerting-backend github.com/redis/go-redis/v9 v9.7.3 // @grafana/alerting-backend github.com/robfig/cron/v3 v3.0.1 // @grafana/grafana-backend-group @@ -161,9 +161,9 @@ require ( github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // @grafana/grafana-operator-experience-squad github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group go.opentelemetry.io/collector/pdata v1.22.0 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // @grafana/plugins-platform-backend go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // @grafana/grafana-operator-experience-squad - go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // @grafana/grafana-backend-group + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel v1.35.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group @@ -175,18 +175,18 @@ require ( go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage go.uber.org/zap v1.27.0 // @grafana/identity-access-team gocloud.dev v0.40.0 // @grafana/grafana-app-platform-squad - golang.org/x/crypto v0.36.0 // @grafana/grafana-backend-group + golang.org/x/crypto v0.37.0 // @grafana/grafana-backend-group golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // @grafana/alerting-backend golang.org/x/mod v0.24.0 // indirect; @grafana/grafana-backend-group - golang.org/x/net v0.37.0 // @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.27.0 // @grafana/identity-access-team - golang.org/x/sync v0.12.0 // @grafana/alerting-backend - golang.org/x/text v0.23.0 // @grafana/grafana-backend-group - golang.org/x/time v0.10.0 // @grafana/grafana-backend-group - golang.org/x/tools v0.31.0 // indirect; @grafana/grafana-as-code + golang.org/x/net v0.39.0 // @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/oauth2 v0.29.0 // @grafana/identity-access-team + golang.org/x/sync v0.13.0 // @grafana/alerting-backend + golang.org/x/text v0.24.0 // @grafana/grafana-backend-group + golang.org/x/time v0.11.0 // @grafana/grafana-backend-group + golang.org/x/tools v0.32.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.15.1 // @grafana/oss-big-tent google.golang.org/api v0.223.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.71.0 // @grafana/plugins-platform-backend + google.golang.org/grpc v1.71.1 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.6 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group @@ -551,8 +551,8 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect diff --git a/go.sum b/go.sum index 808c51502de..191d7ed65ac 100644 --- a/go.sum +++ b/go.sum @@ -1565,8 +1565,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 h1:5mOChD6fbkxeafK1iuy/iO/qKLyHeougMTtRJSgvAUM= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 h1:qT0D7AIV0GRu8JUrSJYuyzj86kqLgksKQjwD++DqyOM= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d h1:TDVZemfYeJHPyXeYCnqL7BQqsa+mpaZYth/Qm3TKaT8= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us= github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= @@ -1599,8 +1599,8 @@ github.com/grafana/grafana-google-sdk-go v0.2.1 h1:XeFdKnkXBjOJjXc1gf4iMx4h5aCHT github.com/grafana/grafana-google-sdk-go v0.2.1/go.mod h1:RiITSHwBhqVTTd3se3HQq5Ncs/wzzhTB9OK5N0J0PEU= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= +github.com/grafana/grafana-plugin-sdk-go v0.275.0 h1:icGmZG91lVqIo79w/pSki6N44d3IjOjTfsfQPfu4THU= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 h1:mG/6nDlEBVxWlo2GQJVASzucw3ByPIBsec06XcPrjgQ= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 h1:w42GlvkmHG4nM/p1kb2nKmROVP+AHtL3qWEYMhnhCVM= @@ -2214,8 +2214,8 @@ github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= @@ -2532,16 +2532,16 @@ go.opentelemetry.io/collector/pdata v1.22.0 h1:3yhjL46NLdTMoP8rkkcE9B0pzjf2973cr go.opentelemetry.io/collector/pdata v1.22.0/go.mod h1:nLLf6uDg8Kn5g3WNZwGyu8+kf77SwOqQvMTb5AXEbEY= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= @@ -2637,8 +2637,7 @@ golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2784,8 +2783,7 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2821,8 +2819,8 @@ golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4 golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2844,8 +2842,8 @@ golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2972,8 +2970,7 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2989,8 +2986,7 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -3010,16 +3006,14 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -3097,8 +3091,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -3404,8 +3397,8 @@ google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwS google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/go.work.sum b/go.work.sum index 22028db7e3a..c862bfc269b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -575,6 +575,7 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= @@ -1495,6 +1496,7 @@ github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/j github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/statsd_exporter v0.26.0 h1:SQl3M6suC6NWQYEzOvIv+EF6dAMYEqIuZy+o4H9F5Ig= @@ -1774,6 +1776,7 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.58.0/go.mod h1:uosvgpqTcTXtcPQORTbEkZNDQTCDOgTz1fe6aLSyqrQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= @@ -1865,6 +1868,7 @@ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ss golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= @@ -1908,6 +1912,8 @@ golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= @@ -1915,8 +1921,10 @@ golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1939,20 +1947,27 @@ golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -1969,6 +1984,7 @@ golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE= @@ -2064,6 +2080,7 @@ google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFN google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/package.json b/package.json index 32a78ece8b8..e5bd0bfb49a 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,9 @@ "packages:prepare": "lerna version --no-push --no-git-tag-version --force-publish --exact", "packages:pack": "mkdir -p ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", "packages:typecheck": "nx run-many -t typecheck --projects='tag:scope:package'", - "prettier:check": "prettier --check --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx,json,js}\"", - "prettier:checkDocs": "prettier --check --list-different=false --log-level=warn \"docs/**/*.md\" \"*.md\" \"packages/**/*.{ts,tsx,scss,md,mdx,json}\"", - "prettier:write": "prettier --list-different \"**/*.{js,ts,tsx,scss,md,mdx,json}\" --write", + "prettier:check": "prettier --check --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx,json,js,cjs}\"", + "prettier:checkDocs": "prettier --check --list-different=false --log-level=warn \"docs/**/*.md\" \"*.md\" \"packages/**/*.{ts,tsx,scss,md,mdx,json,js,cjs}\"", + "prettier:write": "prettier --list-different \"**/*.{js,ts,tsx,scss,md,mdx,json,cjs}\" --write", "start": "NODE_ENV=dev nx exec -- webpack --config scripts/webpack/webpack.dev.js --watch", "start:liveReload": "yarn start -- --env liveReload=1", "start:noTsCheck": "yarn start -- --env noTsCheck=1", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 154c070f860..b8a364f19a6 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -132,11 +132,6 @@ export interface FeatureToggles { */ influxdbRunQueriesInParallel?: boolean; /** - * Enables running Prometheus queries in parallel - * @default true - */ - prometheusRunQueriesInParallel?: boolean; - /** * Changes logs responses from Loki to be compliant with the dataplane specification. */ lokiLogsDataplane?: boolean; @@ -296,10 +291,6 @@ export interface FeatureToggles { */ externalCorePlugins?: boolean; /** - * Sends metrics of public grafana packages usage by plugins - */ - pluginsAPIMetrics?: boolean; - /** * Automatic service account and token setup for plugins */ externalServiceAccounts?: boolean; @@ -336,9 +327,22 @@ export interface FeatureToggles { kubernetesDashboards?: boolean; /** * Route the folder and dashboard service requests to k8s + * @default true */ kubernetesClientDashboardsFolders?: boolean; /** + * Disable schema validation for dashboards/v1 + */ + dashboardDisableSchemaValidationV1?: boolean; + /** + * Disable schema validation for dashboards/v2 + */ + dashboardDisableSchemaValidationV2?: boolean; + /** + * Log schema validation errors so they can be analyzed later + */ + dashboardSchemaValidationLogging?: boolean; + /** * Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) */ datasourceQueryTypes?: boolean; @@ -448,11 +452,6 @@ export interface FeatureToggles { */ logsInfiniteScrolling?: boolean; /** - * Enables the new Grafana Metrics Drilldown core app - * @default true - */ - exploreMetrics?: boolean; - /** * Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule * @default true */ @@ -910,11 +909,6 @@ export interface FeatureToggles { */ elasticsearchImprovedParsing?: boolean; /** - * Use the externalized Grafana Metrics Drilldown (formerly known as Explore Metrics) app plugin - * @default true - */ - exploreMetricsUseExternalAppPlugin?: boolean; - /** * Shows defined connections for a data source in the plugins detail page */ datasourceConnectionsTab?: boolean; @@ -944,6 +938,10 @@ export interface FeatureToggles { */ useScopesNavigationEndpoint?: boolean; /** + * Enable scope search to include all levels of the scope node tree + */ + scopeSearchAllLevels?: boolean; + /** * Enables the alert rule version history restore feature * @default true */ diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index f33a67d8bf9..c7a1df01b23 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -167,6 +167,13 @@ export type PluginExtensionEventHelpers = { context?: Readonly; // Opens a modal dialog and renders the provided React component inside it openModal: (options: PluginExtensionOpenModalOptions) => void; + /** + * @internal + * Opens the extension sidebar with the registered component. + * @param componentTitle The title of the component to be opened in the sidebar. + * @param props The props to be passed to the component. + */ + openSidebar: (componentTitle: string, props?: Record) => void; }; // Extension Points & Contexts diff --git a/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs b/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs index c8a71bdfbed..27ad2f2a4f2 100644 --- a/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs +++ b/packages/grafana-eslint-rules/rules/no-aria-label-e2e-selectors.cjs @@ -15,7 +15,9 @@ const { ESLintUtils } = require('@typescript-eslint/utils'); const GRAFANA_E2E_PACKAGE_NAME = '@grafana/e2e-selectors'; -const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); +const createRule = ESLintUtils.RuleCreator( + (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` +); // A relative simple lint rule that will look of the `selectors` export from @grafana/e2e-selectors // is used in an aria-label diff --git a/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs b/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs index 6386ced4437..d145bf970bb 100644 --- a/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs +++ b/packages/grafana-eslint-rules/rules/no-border-radius-literal.cjs @@ -1,7 +1,9 @@ // @ts-check const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); -const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); +const createRule = ESLintUtils.RuleCreator( + (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` +); const borderRadiusRule = createRule({ create(context) { diff --git a/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs b/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs index a1b279fb861..a8a691c9ed7 100644 --- a/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs +++ b/packages/grafana-eslint-rules/rules/no-unreduced-motion.cjs @@ -1,7 +1,9 @@ // @ts-check const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); -const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); +const createRule = ESLintUtils.RuleCreator( + (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` +); const restrictedProperties = ['animation', 'transition']; @@ -13,16 +15,13 @@ const rule = createRule({ create(context) { return { CallExpression(node) { - if ( - node.callee.type === AST_NODE_TYPES.Identifier && - node.callee.name === 'css' - ) { + if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === 'css') { const cssObjects = node.arguments.flatMap((node) => { switch (node.type) { case AST_NODE_TYPES.ObjectExpression: return [node]; case AST_NODE_TYPES.ArrayExpression: - return node.elements.filter(v => v?.type === AST_NODE_TYPES.ObjectExpression); + return node.elements.filter((v) => v?.type === AST_NODE_TYPES.ObjectExpression); default: return []; } @@ -55,7 +54,8 @@ const rule = createRule({ description: 'Check if animation or transition properties are used directly.', }, messages: { - noUnreducedMotion: 'Avoid direct use of `animation*` or `transition*` properties. Use the `handleMotion` utility function from theme.transitions or wrap in a `prefers-reduced-motion` media query.', + noUnreducedMotion: + 'Avoid direct use of `animation*` or `transition*` properties. Use the `handleMotion` utility function from theme.transitions or wrap in a `prefers-reduced-motion` media query.', }, schema: [], }, diff --git a/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs b/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs index 4e0468dffc5..2ea64533af2 100644 --- a/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs +++ b/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs @@ -11,7 +11,7 @@ const { canBeFixed, elementIsTrans, shouldBeFixed, - isStringLiteral, + isStringNonAlphanumeric, } = require('./translation-utils.cjs'); const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); @@ -32,10 +32,12 @@ const noUntranslatedStrings = createRule({ return; } - const isUntranslatedProp = - (node.value.type === 'Literal' && node.value.value !== '') || - (node.value.type === AST_NODE_TYPES.JSXExpressionContainer && - ((isStringLiteral(node.value.expression) && node.value.expression.value !== '') || node.value.expression.type === 'TemplateLiteral')); + const nodeValue = getNodeValue(node); + const isAlphaNumeric = !isStringNonAlphanumeric(nodeValue); + const isTemplateLiteral = + node.value.type === AST_NODE_TYPES.JSXExpressionContainer && node.value.expression.type === 'TemplateLiteral'; + + const isUntranslatedProp = (nodeValue.trim() && isAlphaNumeric) || isTemplateLiteral; if (isUntranslatedProp) { const errorShouldBeFixed = shouldBeFixed(context); @@ -63,8 +65,8 @@ const noUntranslatedStrings = createRule({ const children = node.children; const untranslatedTextNodes = children.filter((child) => { if (child.type === AST_NODE_TYPES.JSXText) { - const hasValue = child.value.trim(); - if (!hasValue) { + const nodeValue = child.value.trim(); + if (!nodeValue || isStringNonAlphanumeric(nodeValue)) { return false; } const ancestors = context.sourceCode.getAncestors(node); diff --git a/packages/grafana-eslint-rules/rules/theme-token-usage.cjs b/packages/grafana-eslint-rules/rules/theme-token-usage.cjs index 506f8db8c59..51e46b00999 100644 --- a/packages/grafana-eslint-rules/rules/theme-token-usage.cjs +++ b/packages/grafana-eslint-rules/rules/theme-token-usage.cjs @@ -1,7 +1,9 @@ // @ts-check const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); -const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`); +const createRule = ESLintUtils.RuleCreator( + (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` +); const themeTokenUsage = createRule({ create(context) { diff --git a/packages/grafana-eslint-rules/rules/translation-utils.cjs b/packages/grafana-eslint-rules/rules/translation-utils.cjs index e884738726a..a5e3aad8c63 100644 --- a/packages/grafana-eslint-rules/rules/translation-utils.cjs +++ b/packages/grafana-eslint-rules/rules/translation-utils.cjs @@ -26,11 +26,8 @@ const elementIsTrans = (node) => { * @param {Node} node */ const isStringLiteral = (node) => { - return ( - node.type === AST_NODE_TYPES.Literal && - typeof node.value === 'string' - ); -} + return node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'; +}; /** * Converts a string to kebab case @@ -44,6 +41,14 @@ function toKebabCase(str) { .replace(/\s+/g, '-'); } +/** + * Checks if a string is non-alphanumeric + * @param {string} str The string to check + * @returns {boolean} + */ +function isStringNonAlphanumeric(str) { + return !/[a-zA-Z0-9]/.test(str); +} /** * Checks if we _should_ fix an error automatically * @param {RuleContextWithOptions} context @@ -309,10 +314,7 @@ function getNodeValue(node) { if (node.type === AST_NODE_TYPES.JSXAttribute && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer) { // this condition is basically `isStringLiteral`, but we can't use the function // else it doesn't narrow the type correctly :( - if ( - node.value.expression.type === AST_NODE_TYPES.Literal && - typeof node.value.expression.value === 'string' - ) { + if (node.value.expression.type === AST_NODE_TYPES.Literal && typeof node.value.expression.value === 'string') { return node.value.expression.value; } } @@ -327,5 +329,5 @@ module.exports = { canBeFixed, shouldBeFixed, elementIsTrans, - isStringLiteral, + isStringNonAlphanumeric, }; diff --git a/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js b/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js index 970f0065eba..35b2d1cb251 100644 --- a/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js +++ b/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js @@ -68,6 +68,18 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, { name: 'Variable interpolation', code: `
{variable}
`, }, + { + name: 'Entirely non-alphanumeric text (prop)', + code: `
`, + }, + { + name: 'Entirely non-alphanumeric text', + code: `
-
`, + }, + { + name: 'Non-alphanumeric siblings', + code: `
({variable})
`, + }, ], invalid: [ /** @@ -416,12 +428,6 @@ const Foo = () => { ], }, - { - name: 'Cannot fix entirely non-alphanumeric text', - code: `const Foo = () =>
-
`, - filename, - errors: [{ messageId: 'noUntranslatedStrings' }], - }, { name: 'Cannot fix text with expression sibling', code: `const Foo = () =>
{name} Hello
`, diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 4e15cffeb3d..73faa5df734 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -64,7 +64,6 @@ LibraryPanelSpec: { id: number // Title for the library panel in the dashboard title: string - libraryPanel: LibraryPanelRef } diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 1a4fad82b96..2c633337b01 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -67,7 +67,7 @@ export const defaultAnnotationPanelFilter = (): AnnotationPanelFilter => ({ // "Off" for no shared crosshair or tooltip (default). // "Crosshair" for shared crosshair. // "Tooltip" for shared crosshair AND shared tooltip. -export type DashboardCursorSync = "Off" | "Crosshair" | "Tooltip"; +export type DashboardCursorSync = "Crosshair" | "Tooltip" | "Off"; export const defaultDashboardCursorSync = (): DashboardCursorSync => ("Off"); @@ -282,7 +282,7 @@ export interface FieldConfig { description?: string; // An explicit path to the field in the datasource. When the frame meta includes a path, // This will default to `${frame.meta.path}/${field.name} - // + // // When defined, this value can be used as an identifier within the datasource scope, and // may be used to update the results path?: string; @@ -1367,7 +1367,6 @@ export const defaultMetricFindValue = (): MetricFindValue => ({ }); export interface Spec { - // Title of dashboard. annotations: AnnotationQueryKind[]; // Configuration of dashboard cursor sync behavior. // "Off" for no shared crosshair or tooltip (default). @@ -1413,3 +1412,4 @@ export const defaultSpec = (): Spec => ({ title: "", variables: [], }); + diff --git a/packages/grafana-ui/src/components/Combobox/useOptions.ts b/packages/grafana-ui/src/components/Combobox/useOptions.ts index ad160d6fb21..d35b8157b7a 100644 --- a/packages/grafana-ui/src/components/Combobox/useOptions.ts +++ b/packages/grafana-ui/src/components/Combobox/useOptions.ts @@ -65,10 +65,12 @@ export function useOptions(rawOptions: AsyncOptions>) => { let currentOptions: Array> = opts; if (createCustomValue && userTypedSearch) { - //Since the label of a normal option does not have to match its value and a custom option has the same value and label, - //we just focus on the value to check if the option already exists + // Since the label of a normal option does not have to match its value and a custom option has the same value and label, + // we just focus on the value to check if the option already exists const customValueExists = opts.some((opt) => opt.value === userTypedSearch); if (!customValueExists) { + // Make sure to clone the array first to avoid mutating the original array! + currentOptions = currentOptions.slice(); currentOptions.unshift({ label: userTypedSearch, value: userTypedSearch as T, diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/GeoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/GeoCell.tsx new file mode 100644 index 00000000000..40e5bc04778 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/GeoCell.tsx @@ -0,0 +1,43 @@ +import { css } from '@emotion/css'; +import WKT from 'ol/format/WKT'; +import { Geometry } from 'ol/geom'; + +import { useStyles2 } from '../../../../themes'; +import { GeoCellProps } from '../types'; + +export function GeoCell({ value, justifyContent, height }: GeoCellProps) { + const styles = useStyles2(getStyles); + + let disp = ''; + + if (value instanceof Geometry) { + disp = new WKT().writeGeometry(value, { + featureProjection: 'EPSG:3857', + dataProjection: 'EPSG:4326', + }); + } else if (value != null) { + disp = `${value}`; + } + + return ( +
+
+ {disp} +
+
+ ); +} + +const getStyles = () => ({ + cell: css({ + height: '100%', + display: 'flex', + alignItems: 'center', + padding: '0 8px', + }), + cellText: css({ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }), +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx index 1a1c3bbca5a..f9a91fa226c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx @@ -1,12 +1,15 @@ import { css } from '@emotion/css'; +import { WKT } from 'ol/format'; +import { Geometry } from 'ol/geom'; import { ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { FieldType, GrafanaTheme2, isDataFrame, isTimeSeriesFrame } from '@grafana/data'; import { TableAutoCellOptions, TableCellDisplayMode } from '@grafana/schema'; import { useStyles2 } from '../../../../themes'; import { t } from '../../../../utils/i18n'; import { IconButton } from '../../../IconButton/IconButton'; +// import { GeoCell } from '../../Cells/GeoCell'; import { TableCellInspectorMode } from '../../TableCellInspector'; import { CellColors, @@ -21,6 +24,7 @@ import { ActionsCell } from './ActionsCell'; import AutoCell from './AutoCell'; import { BarGaugeCell } from './BarGaugeCell'; import { DataLinksCell } from './DataLinksCell'; +import { GeoCell } from './GeoCell'; import { ImageCell } from './ImageCell'; import { JSONCell } from './JSONCell'; import { SparklineCell } from './SparklineCell'; @@ -78,52 +82,31 @@ export function TableCellNG(props: TableCellNGProps) { } }, [divWidthRef.current]); // eslint-disable-line react-hooks/exhaustive-deps + // Common props for all cells + const commonProps = { + value, + field, + rowIdx, + justifyContent, + }; + // Get the correct cell type let cell: ReactNode = null; switch (cellType) { case TableCellDisplayMode.Sparkline: - cell = ( - - ); + cell = ; break; case TableCellDisplayMode.Gauge: case TableCellDisplayMode.BasicGauge: case TableCellDisplayMode.GradientGauge: case TableCellDisplayMode.LcdGauge: - cell = ( - - ); + cell = ; break; case TableCellDisplayMode.Image: - cell = ( - - ); + cell = ; break; case TableCellDisplayMode.JSONView: - cell = ; + cell = ; break; case TableCellDisplayMode.DataLinks: cell = ; @@ -137,15 +120,22 @@ export function TableCellNG(props: TableCellNGProps) { break; case TableCellDisplayMode.Auto: default: - cell = ( - - ); + // Handle auto cell type detection + if (field.type === FieldType.geo) { + cell = ; + } else if (field.type === FieldType.frame) { + const firstValue = field.values[0]; + if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) { + cell = ; + } else { + cell = ; + } + } else if (field.type === FieldType.other) { + cell = ; + } else { + cell = ; + } + break; } const handleMouseEnter = () => { @@ -200,12 +190,22 @@ export function TableCellNG(props: TableCellNGProps) { name="eye" tooltip={t('grafana-ui.table.cell-inspect-tooltip', 'Inspect value')} onClick={() => { + let inspectValue = value; + let mode = TableCellInspectorMode.text; + + if (field.type === FieldType.geo && value instanceof Geometry) { + inspectValue = new WKT().writeGeometry(value, { + featureProjection: 'EPSG:3857', + dataProjection: 'EPSG:4326', + }); + mode = TableCellInspectorMode.code; + } else if (cellType === TableCellDisplayMode.JSONView) { + mode = TableCellInspectorMode.code; + } + setContextMenuProps({ - value: String(value ?? ''), - mode: - cellType === TableCellDisplayMode.JSONView - ? TableCellInspectorMode.code - : TableCellInspectorMode.text, + value: String(inspectValue ?? ''), + mode, }); setIsInspecting(true); }} diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 994409a6a4d..c2a760b4f49 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -207,6 +207,12 @@ export interface DataLinksCellProps { rowIdx: number; } +export interface GeoCellProps { + value: TableCellValue; + justifyContent: Property.JustifyContent; + height: number; +} + export interface ActionCellProps { actions?: ActionModel[]; } diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index a80a758813a..a9c5908dda4 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -515,7 +515,9 @@ export interface MapFrameToGridOptions extends TableNGProps { } /* ----------------------------- Data grid comparator ---------------------------- */ -const compare = new Intl.Collator('en', { sensitivity: 'base' }).compare; +// The numeric: true option is used to sort numbers as strings correctly. It recognizes numeric sequences +// within strings and sorts numerically instead of lexicographically. +const compare = new Intl.Collator('en', { sensitivity: 'base', numeric: true }).compare; export function getComparator(sortColumnType: FieldType): Comparator { switch (sortColumnType) { case FieldType.time: diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index ea469465306..5a0b6ec250d 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,7 +4,7 @@ go 1.24.2 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 + github.com/grafana/grafana-plugin-sdk-go v0.275.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -101,7 +101,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -122,10 +122,10 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect @@ -135,22 +135,22 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 3bbfd2f8599..75268bf17ee 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -136,8 +136,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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= +github.com/grafana/grafana-plugin-sdk-go v0.275.0 h1:icGmZG91lVqIo79w/pSki6N44d3IjOjTfsfQPfu4THU= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= @@ -264,8 +264,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= @@ -354,14 +354,14 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= @@ -397,8 +397,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= @@ -419,18 +419,18 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -445,16 +445,16 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -466,8 +466,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -493,8 +493,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 684bfd3dfe2..d2479b669a7 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -36,13 +36,13 @@ require ( go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 8822bfb9964..6b131bd8a33 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -89,8 +89,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -103,15 +103,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -122,8 +122,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -135,8 +135,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -149,8 +149,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apimachinery/utils/errors.go b/pkg/apimachinery/utils/errors.go new file mode 100644 index 00000000000..24c05482a15 --- /dev/null +++ b/pkg/apimachinery/utils/errors.go @@ -0,0 +1,20 @@ +package utils + +import ( + "errors" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Extract the status from an APIStatus error +func ExtractApiErrorStatus(err error) (metav1.Status, bool) { + if err == nil { + return metav1.Status{}, false + } + if statusErr, ok := err.(apierrors.APIStatus); ok && errors.As(err, &statusErr) { + return statusErr.Status(), true + } + + return metav1.Status{}, false +} diff --git a/pkg/apis/folder/go.mod b/pkg/apis/folder/go.mod index b3c9f299f55..74ad25b0722 100644 --- a/pkg/apis/folder/go.mod +++ b/pkg/apis/folder/go.mod @@ -26,8 +26,8 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/apis/folder/go.sum b/pkg/apis/folder/go.sum index 7bb082e5c2a..1880739618e 100644 --- a/pkg/apis/folder/go.sum +++ b/pkg/apis/folder/go.sum @@ -67,8 +67,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -77,8 +77,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/pkg/apis/provisioning/v0alpha1/types.go b/pkg/apis/provisioning/v0alpha1/types.go index 6996422fba6..9c069f96ad4 100644 --- a/pkg/apis/provisioning/v0alpha1/types.go +++ b/pkg/apis/provisioning/v0alpha1/types.go @@ -394,11 +394,14 @@ type TestResults struct { // Is the connection healthy Success bool `json:"success"` - // Error descriptions - Errors []string `json:"errors,omitempty"` + // Field related errors + Errors []ErrorDetails `json:"errors,omitempty"` +} - // Optional details - Details *common.Unstructured `json:"details,omitempty"` +type ErrorDetails struct { + Type metav1.CauseType `json:"type"` + Field string `json:"field,omitempty"` + Detail string `json:"detail,omitempty"` } // HistoryList is a list of versions of a resource diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go index 8e09d703d09..2beb7403f6a 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.deepcopy.go @@ -27,6 +27,22 @@ func (in *Author) DeepCopy() *Author { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ErrorDetails) DeepCopyInto(out *ErrorDetails) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ErrorDetails. +func (in *ErrorDetails) DeepCopy() *ErrorDetails { + if in == nil { + return nil + } + out := new(ErrorDetails) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExportJobOptions) DeepCopyInto(out *ExportJobOptions) { *out = *in @@ -849,13 +865,9 @@ func (in *TestResults) DeepCopyInto(out *TestResults) { out.TypeMeta = in.TypeMeta if in.Errors != nil { in, out := &in.Errors, &out.Errors - *out = make([]string, len(*in)) + *out = make([]ErrorDetails, len(*in)) copy(*out, *in) } - if in.Details != nil { - in, out := &in.Details, &out.Details - *out = (*in).DeepCopy() - } return } diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 184ea75003a..4f9483d09b7 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -15,6 +15,7 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.Author": schema_pkg_apis_provisioning_v0alpha1_Author(ref), + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ErrorDetails": schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ExportJobOptions": schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileItem": schema_pkg_apis_provisioning_v0alpha1_FileItem(ref), "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.FileList": schema_pkg_apis_provisioning_v0alpha1_FileList(ref), @@ -88,6 +89,38 @@ func schema_pkg_apis_provisioning_v0alpha1_Author(ref common.ReferenceCallback) } } +func schema_pkg_apis_provisioning_v0alpha1_ErrorDetails(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "field": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "detail": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type"}, + }, + }, + } +} + func schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1909,31 +1942,24 @@ func schema_pkg_apis_provisioning_v0alpha1_TestResults(ref common.ReferenceCallb }, "errors": { SchemaProps: spec.SchemaProps{ - Description: "Error descriptions", + Description: "Field related errors", Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ErrorDetails"), }, }, }, }, }, - "details": { - SchemaProps: spec.SchemaProps{ - Description: "Optional details", - Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), - }, - }, }, Required: []string{"code", "success"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1.ErrorDetails"}, } } diff --git a/pkg/apis/secret/go.mod b/pkg/apis/secret/go.mod index 2491a943981..38321629f72 100644 --- a/pkg/apis/secret/go.mod +++ b/pkg/apis/secret/go.mod @@ -5,7 +5,7 @@ go 1.24.2 require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.71.0 + google.golang.org/grpc v1.71.1 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.32.3 @@ -56,7 +56,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/cobra v1.9.1 // indirect @@ -67,7 +67,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect @@ -78,13 +78,13 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect diff --git a/pkg/apis/secret/go.sum b/pkg/apis/secret/go.sum index bbb249afb07..b42e0c9f53b 100644 --- a/pkg/apis/secret/go.sum +++ b/pkg/apis/secret/go.sum @@ -141,8 +141,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= @@ -192,8 +192,8 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -225,8 +225,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -243,11 +243,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -259,16 +259,16 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -279,8 +279,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -302,8 +302,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 5c1576c3b00..896bcc3ced0 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -9,7 +9,7 @@ require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 github.com/prometheus/client_golang v1.21.1 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 k8s.io/apimachinery v0.32.3 @@ -61,7 +61,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/cobra v1.9.1 // indirect @@ -73,7 +73,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect go.etcd.io/etcd/client/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect @@ -82,17 +82,17 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 1c098c7c26b..f1ae3618260 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -145,8 +145,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= @@ -198,12 +198,12 @@ go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= @@ -233,8 +233,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -251,11 +251,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -267,16 +267,16 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -287,8 +287,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -310,8 +310,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 977b0491ddd..1292891fc24 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -21,19 +21,19 @@ require ( github.com/stretchr/testify v1.10.0 // @grafana/grafana-backend-group github.com/urfave/cli v1.22.16 // @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.6 // @grafana/grafana-backend-group - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect; @grafana/plugins-platform-backend + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect; @grafana/plugins-platform-backend go.opentelemetry.io/otel v1.35.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.35.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/trace v1.35.0 // indirect; @grafana/grafana-backend-group - golang.org/x/crypto v0.36.0 // indirect; @grafana/grafana-backend-group + golang.org/x/crypto v0.37.0 // indirect; @grafana/grafana-backend-group golang.org/x/mod v0.24.0 // @grafana/grafana-backend-group - golang.org/x/net v0.37.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources - golang.org/x/oauth2 v0.27.0 // @grafana/identity-access-team - golang.org/x/sync v0.12.0 // indirect; @grafana/alerting-backend - golang.org/x/text v0.23.0 // indirect; @grafana/grafana-backend-group - golang.org/x/time v0.10.0 // indirect; @grafana/grafana-backend-group + golang.org/x/net v0.39.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources + golang.org/x/oauth2 v0.29.0 // @grafana/identity-access-team + golang.org/x/sync v0.13.0 // indirect; @grafana/alerting-backend + golang.org/x/text v0.24.0 // indirect; @grafana/grafana-backend-group + golang.org/x/time v0.11.0 // indirect; @grafana/grafana-backend-group google.golang.org/api v0.223.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.71.0 // indirect; @grafana/plugins-platform-backend + google.golang.org/grpc v1.71.1 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.6 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) @@ -71,7 +71,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - golang.org/x/sys v0.31.0 // indirect + golang.org/x/sys v0.32.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect; @grafana/grafana-backend-group google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 8ddd9042289..bdb0e1c4b93 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -253,8 +253,8 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -297,8 +297,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -315,18 +315,18 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -336,16 +336,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -376,8 +376,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index 5c3bdbbbcc6..caae442331a 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -6,10 +6,10 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/subcommands v1.2.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - golang.org/x/tools v0.31.0 + golang.org/x/tools v0.32.0 ) require ( golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect + golang.org/x/sync v0.13.0 // indirect ) diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index 32aee0c9faf..987989fe45b 100644 --- a/pkg/build/wire/go.sum +++ b/pkg/build/wire/go.sum @@ -6,7 +6,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 403c6eff2d7..dfa5b32c45d 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -45,10 +45,10 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect github.com/yalue/merged_fs v1.3.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 003621ba4ed..d368c5dc8de 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -100,14 +100,14 @@ github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 02bf35aff20..9f32ecfb79b 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -43,10 +43,10 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect github.com/yalue/merged_fs v1.3.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index bb47a190b63..d50129136bc 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -94,18 +94,18 @@ github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY github.com/yalue/merged_fs v1.3.0/go.mod h1:WqqchfVYQyclV2tnR7wtRhBddzBvLVR83Cjw9BKQw0M= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 60a09f4e864..cad984e67fa 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,10 +4,10 @@ go 1.24.2 require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 - github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 + github.com/grafana/grafana-plugin-sdk-go v0.275.0 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.21.1 - github.com/prometheus/common v0.62.0 + github.com/prometheus/common v0.63.0 github.com/prometheus/prometheus v0.301.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.35.0 @@ -98,9 +98,9 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect @@ -111,16 +111,17 @@ require ( go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.223.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/grpc v1.71.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 1ccef0fe3d9..f00a6716adc 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -121,8 +121,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= +github.com/grafana/grafana-plugin-sdk-go v0.275.0 h1:icGmZG91lVqIo79w/pSki6N44d3IjOjTfsfQPfu4THU= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= 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/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -231,8 +231,8 @@ github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGC github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/prometheus v0.301.0 h1:0z8dgegmILivNomCd79RKvVkIols8vBGPKmcIBc7OyY= @@ -292,14 +292,14 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= @@ -329,8 +329,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -342,15 +342,15 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -362,21 +362,21 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -391,8 +391,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/promlib/healthcheck_test.go b/pkg/promlib/healthcheck_test.go index bb8746f5a0a..a58b98e37a2 100644 --- a/pkg/promlib/healthcheck_test.go +++ b/pkg/promlib/healthcheck_test.go @@ -124,8 +124,10 @@ func getPluginContext() backend.PluginContext { User: nil, AppInstanceSettings: nil, DataSourceInstanceSettings: getPromInstanceSettings(), + GrafanaConfig: backend.NewGrafanaCfg(map[string]string{"concurrent_query_count": "10"}), } } + func getPromInstanceSettings() *backend.DataSourceInstanceSettings { return &backend.DataSourceInstanceSettings{ ID: 0, diff --git a/pkg/promlib/models/scope.go b/pkg/promlib/models/scope.go index 767a98a854e..bc2aeb24535 100644 --- a/pkg/promlib/models/scope.go +++ b/pkg/promlib/models/scope.go @@ -10,7 +10,8 @@ import ( ) func init() { - model.NameValidationScheme = model.UTF8Validation + // this is deprecated, we will fix that in a separate PR + model.NameValidationScheme = model.UTF8Validation //nolint:staticcheck } // ApplyFiltersAndGroupBy takes a raw promQL expression, converts the filters into PromQL matchers, and applies these matchers to the parsed expression. It also applies the group by clause to any aggregate expressions in the parsed expression. diff --git a/pkg/promlib/querydata/framing_test.go b/pkg/promlib/querydata/framing_test.go index 3fa48bca92b..be84733ab98 100644 --- a/pkg/promlib/querydata/framing_test.go +++ b/pkg/promlib/querydata/framing_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "testing" @@ -20,7 +21,7 @@ import ( "github.com/grafana/grafana/pkg/promlib/models" ) -var update = true +var update = false func TestRangeResponses(t *testing.T) { tt := []struct { @@ -145,10 +146,42 @@ func runQuery(response []byte, q *backend.QueryDataRequest) (*backend.QueryDataR if err != nil { return nil, err } + + // Create initial response res := &http.Response{ StatusCode: 200, Body: io.NopCloser(bytes.NewReader(response)), + Request: &http.Request{ + URL: &url.URL{ + Path: "api/v1/query_range", + }, + }, } - tCtx.httpProvider.setResponse(res, res) - return tCtx.queryData.Execute(context.Background(), q) + + // Create a proper clone for the exemplar response with a different path + exemplarRes := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewReader(response)), + Request: &http.Request{ + URL: &url.URL{ + Path: "api/v1/query_exemplars", + }, + }, + } + + tCtx.httpProvider.setResponse(res, exemplarRes) + + // Add GrafanaConfig to the context to prevent nil pointer dereference + ctx := backend.WithGrafanaConfig(context.Background(), backend.NewGrafanaCfg(map[string]string{ + "concurrent_query_count": "10", + })) + + // Add a PluginContext with GrafanaConfig to the request + q.PluginContext = backend.PluginContext{ + GrafanaConfig: backend.NewGrafanaCfg(map[string]string{ + "concurrent_query_count": "10", + }), + } + + return tCtx.queryData.Execute(ctx, q) } diff --git a/pkg/promlib/querydata/request.go b/pkg/promlib/querydata/request.go index 7739bf46222..5f3f9556648 100644 --- a/pkg/promlib/querydata/request.go +++ b/pkg/promlib/querydata/request.go @@ -101,45 +101,32 @@ func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) } var ( - hasPromQLScopeFeatureFlag = s.featureToggles.IsEnabled("promQLScope") - hasPrometheusRunQueriesInParallel = s.featureToggles.IsEnabled("prometheusRunQueriesInParallel") + hasPromQLScopeFeatureFlag = s.featureToggles.IsEnabled("promQLScope") + m sync.Mutex ) - if hasPrometheusRunQueriesInParallel { - var ( - m sync.Mutex - ) - - concurrentQueryCount, err := req.PluginContext.GrafanaConfig.ConcurrentQueryCount() - if err != nil { - logger.Debug(fmt.Sprintf("Concurrent Query Count read/parse error: %v", err), "prometheusRunQueriesInParallel") - concurrentQueryCount = 10 - } - - _ = concurrency.ForEachJob(ctx, len(req.Queries), concurrentQueryCount, func(ctx context.Context, idx int) error { - query := req.Queries[idx] - r := s.handleQuery(ctx, query, fromAlert, hasPromQLScopeFeatureFlag, true) - if r != nil { - m.Lock() - result.Responses[query.RefID] = *r - m.Unlock() - } - return nil - }) - } else { - for _, q := range req.Queries { - r := s.handleQuery(ctx, q, fromAlert, hasPromQLScopeFeatureFlag, false) - if r != nil { - result.Responses[q.RefID] = *r - } - } + concurrentQueryCount, err := req.PluginContext.GrafanaConfig.ConcurrentQueryCount() + if err != nil { + logger.Debug(fmt.Sprintf("Concurrent Query Count read/parse error: %v", err), "prometheusRunQueriesInParallel") + concurrentQueryCount = 10 } + _ = concurrency.ForEachJob(ctx, len(req.Queries), concurrentQueryCount, func(ctx context.Context, idx int) error { + query := req.Queries[idx] + r := s.handleQuery(ctx, query, fromAlert, hasPromQLScopeFeatureFlag) + if r != nil { + m.Lock() + result.Responses[query.RefID] = *r + m.Unlock() + } + return nil + }) + return &result, nil } func (s *QueryData) handleQuery(ctx context.Context, bq backend.DataQuery, fromAlert, - hasPromQLScopeFeatureFlag, hasPrometheusRunQueriesInParallel bool) *backend.DataResponse { + hasPromQLScopeFeatureFlag bool) *backend.DataResponse { traceCtx, span := s.tracer.Start(ctx, "datasource.prometheus") defer span.End() query, err := models.Parse(span, bq, s.TimeInterval, s.intervalCalculator, fromAlert, hasPromQLScopeFeatureFlag) @@ -149,14 +136,14 @@ func (s *QueryData) handleQuery(ctx context.Context, bq backend.DataQuery, fromA } } - r := s.fetch(traceCtx, s.client, query, hasPrometheusRunQueriesInParallel) + r := s.fetch(traceCtx, s.client, query) if r == nil { s.log.FromContext(ctx).Debug("Received nil response from runQuery", "query", query.Expr) } return r } -func (s *QueryData) fetch(traceCtx context.Context, client *client.Client, q *models.Query, hasPrometheusRunQueriesInParallel bool) *backend.DataResponse { +func (s *QueryData) fetch(traceCtx context.Context, client *client.Client, q *models.Query) *backend.DataResponse { logger := s.log.FromContext(traceCtx) logger.Debug("Sending query", "start", q.Start, "end", q.End, "step", q.Step, "query", q.Expr /*, "queryTimeout", s.QueryTimeout*/) @@ -171,61 +158,41 @@ func (s *QueryData) fetch(traceCtx context.Context, client *client.Client, q *mo ) if q.InstantQuery { - if hasPrometheusRunQueriesInParallel { - wg.Add(1) - go func() { - defer wg.Done() - res := s.instantQuery(traceCtx, client, q) - m.Lock() - addDataResponse(&res, dr) - m.Unlock() - }() - } else { + wg.Add(1) + go func() { + defer wg.Done() res := s.instantQuery(traceCtx, client, q) + m.Lock() addDataResponse(&res, dr) - } + m.Unlock() + }() } if q.RangeQuery { - if hasPrometheusRunQueriesInParallel { - wg.Add(1) - go func() { - defer wg.Done() - res := s.rangeQuery(traceCtx, client, q) - m.Lock() - addDataResponse(&res, dr) - m.Unlock() - }() - } else { + wg.Add(1) + go func() { + defer wg.Done() res := s.rangeQuery(traceCtx, client, q) + m.Lock() addDataResponse(&res, dr) - } + m.Unlock() + }() } if q.ExemplarQuery { - if hasPrometheusRunQueriesInParallel { - wg.Add(1) - go func() { - defer wg.Done() - res := s.exemplarQuery(traceCtx, client, q) - m.Lock() - if res.Error != nil { - // If exemplar query returns error, we want to only log it and - // continue with other results processing - logger.Error("Exemplar query failed", "query", q.Expr, "err", res.Error) - } - dr.Frames = append(dr.Frames, res.Frames...) - m.Unlock() - }() - } else { + wg.Add(1) + go func() { + defer wg.Done() res := s.exemplarQuery(traceCtx, client, q) + m.Lock() if res.Error != nil { // If exemplar query returns error, we want to only log it and // continue with other results processing logger.Error("Exemplar query failed", "query", q.Expr, "err", res.Error) } dr.Frames = append(dr.Frames, res.Frames...) - } + m.Unlock() + }() } wg.Wait() diff --git a/pkg/promlib/querydata/request_test.go b/pkg/promlib/querydata/request_test.go index d462b82d693..64797c18873 100644 --- a/pkg/promlib/querydata/request_test.go +++ b/pkg/promlib/querydata/request_test.go @@ -96,15 +96,26 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { // Test fields require.Len(t, res, 2) - require.Equal(t, res[0].Name, "exemplar") - require.Equal(t, res[0].Fields[0].Name, "Time") - require.Equal(t, res[0].Fields[1].Name, "Value") - require.Len(t, res[0].Fields, 6) + // Find the exemplar frame + var exemplarFrame *data.Frame + var rangeFrame *data.Frame + for _, frame := range res { + if frame.Name == "exemplar" { + exemplarFrame = frame + } else { + rangeFrame = frame + } + } + require.NotNil(t, exemplarFrame) + require.NotNil(t, rangeFrame) + require.Equal(t, "Time", exemplarFrame.Fields[0].Name) + require.Equal(t, "Value", exemplarFrame.Fields[1].Name) + require.Len(t, exemplarFrame.Fields, 6) // Test correct values (sampled to 2) - require.Equal(t, res[0].Fields[1].Len(), 4) - require.Equal(t, res[0].Fields[1].At(0), 0.009545445) - require.Equal(t, res[0].Fields[1].At(3), 0.003535405) + require.Equal(t, 4, exemplarFrame.Fields[1].Len()) + require.Equal(t, 0.009545445, exemplarFrame.Fields[1].At(0)) + require.Equal(t, 0.003535405, exemplarFrame.Fields[1].At(3)) }) t.Run("matrix response should be parsed normally", func(t *testing.T) { @@ -379,6 +390,11 @@ func executeWithHeaders(tctx *testContext, query backend.DataQuery, rqr any, eqr req := backend.QueryDataRequest{ Queries: []backend.DataQuery{query}, Headers: headers, + PluginContext: backend.PluginContext{ + GrafanaConfig: backend.NewGrafanaCfg(map[string]string{ + "concurrent_query_count": "10", + }), + }, } rangeRes, err := toAPIResponse(rqr) @@ -399,7 +415,12 @@ func executeWithHeaders(tctx *testContext, query backend.DataQuery, rqr any, eqr }() tctx.httpProvider.setResponse(rangeRes, exemplarRes) - res, err := tctx.queryData.Execute(context.Background(), &req) + // Create context with GrafanaConfig + ctx := backend.WithGrafanaConfig(context.Background(), backend.NewGrafanaCfg(map[string]string{ + "concurrent_query_count": "10", + })) + + res, err := tctx.queryData.Execute(ctx, &req) if err != nil { return nil, err } @@ -505,7 +526,37 @@ func (p *fakeHttpClientProvider) GetTransport(opts ...httpclient.Options) (http. func (p *fakeHttpClientProvider) setResponse(rangeRes *http.Response, exemplarRes *http.Response) { p.rangeRes = rangeRes - p.exemplarRes = exemplarRes + + // Create a proper clone manually ensuring we have a fresh response + if exemplarRes != nil { + bodyBytes, _ := io.ReadAll(exemplarRes.Body) + err := exemplarRes.Body.Close() // Close the original + if err != nil { + fmt.Println(fmt.Errorf("exemplarRes body close error: %v", err)) + return + } + + // Create a new request if the original has one + var newRequest *http.Request + if exemplarRes.Request != nil { + newRequest = &http.Request{ + Method: exemplarRes.Request.Method, + URL: exemplarRes.Request.URL, + Header: exemplarRes.Request.Header.Clone(), + } + } + + // Create a new response with the same data but new body + p.exemplarRes = &http.Response{ + StatusCode: exemplarRes.StatusCode, + Body: io.NopCloser(bytes.NewReader(bodyBytes)), + Request: newRequest, + Header: exemplarRes.Header.Clone(), + } + + // Reset the original body with a new reader + exemplarRes.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + } } func (p *fakeHttpClientProvider) RoundTrip(req *http.Request) (*http.Response, error) { diff --git a/pkg/promlib/testdata/exemplar.result.golden.jsonc b/pkg/promlib/testdata/exemplar.result.golden.jsonc index 3cb50356f90..4430212d03e 100644 --- a/pkg/promlib/testdata/exemplar.result.golden.jsonc +++ b/pkg/promlib/testdata/exemplar.result.golden.jsonc @@ -29,6 +29,36 @@ // +-----------------------------------+-----------------+--------------------------------------------+----------------+-------------------+-------------------+----------------+----------------+--------------------+----------------+------------------+-----------------+-------------------+----------------------------------+ // // +// +// Frame[1] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "custom": { +// "resultType": "exemplar" +// } +// } +// Name: exemplar +// Dimensions: 14 Fields by 62 Rows +// +-----------------------------------+-----------------+--------------------------------------------+----------------+-------------------+-------------------+----------------+----------------+--------------------+----------------+------------------+-----------------+-------------------+----------------------------------+ +// | Name: Time | Name: Value | Name: __name__ | Name: group | Name: http_method | Name: http_target | Name: instance | Name: le | Name: service | Name: source | Name: span_kind | Name: span_name | Name: span_status | Name: traceID | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []time.Time | Type: []float64 | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | +// +-----------------------------------+-----------------+--------------------------------------------+----------------+-------------------+-------------------+----------------+----------------+--------------------+----------------+------------------+-----------------+-------------------+----------------------------------+ +// | 2022-06-01 12:28:31.809 +0000 UTC | 0.001162 | traces_spanmetrics_duration_seconds_bucket | mythical | | | f1c32fe505c3 | 0.002 | mythical-server | tempo | SPAN_KIND_CLIENT | pg.query:INSERT | STATUS_CODE_ERROR | 94809f37ec6cc984f2eb2b14a9f8036e | +// | 2022-06-01 12:28:36.81 +0000 UTC | 0.134971 | traces_spanmetrics_duration_seconds_bucket | mythical | | | f1c32fe505c3 | 0.256 | mythical-requester | tempo | SPAN_KIND_CLIENT | requester | STATUS_CODE_OK | 46e15ae41852149068aeb26c64643c29 | +// | 2022-06-01 12:28:36.81 +0000 UTC | 0.090381 | traces_spanmetrics_duration_seconds_bucket | mythical | POST | /loki/api/v1/push | f1c32fe505c3 | 0.128 | mythical-server | tempo | SPAN_KIND_CLIENT | HTTP POST | STATUS_CODE_OK | 46e15ae41852149068aeb26c64643c29 | +// | 2022-06-01 12:28:36.81 +0000 UTC | 0.029188 | traces_spanmetrics_duration_seconds_bucket | mythical | GET | /manticore | f1c32fe505c3 | 0.032 | mythical-server | tempo | SPAN_KIND_SERVER | GET /:endpoint | STATUS_CODE_OK | 1e58dc2083eceb454846f9ab343a79ff | +// | 2022-06-01 12:28:41.809 +0000 UTC | 0.05719 | traces_spanmetrics_duration_seconds_bucket | mythical | | | f1c32fe505c3 | 0.064 | mythical-requester | tempo | SPAN_KIND_CLIENT | requester | STATUS_CODE_OK | d6ff032e85e6e5719a1f6b923042765f | +// | 2022-06-01 12:28:46.809 +0000 UTC | 0.071953 | traces_spanmetrics_duration_seconds_bucket | mythical | | | f1c32fe505c3 | 0.128 | mythical-requester | tempo | SPAN_KIND_CLIENT | requester | STATUS_CODE_OK | b12280a11924d98f7001f9b03779c6bc | +// | 2022-06-01 12:28:46.809 +0000 UTC | 0.041812 | traces_spanmetrics_duration_seconds_bucket | mythical | GET | /unicorn | f1c32fe505c3 | 0.064 | mythical-server | tempo | SPAN_KIND_SERVER | GET /:endpoint | STATUS_CODE_OK | b12280a11924d98f7001f9b03779c6bc | +// | 2022-06-01 12:28:56.809 +0000 UTC | 0.013244 | traces_spanmetrics_duration_seconds_bucket | mythical | | | f1c32fe505c3 | 0.016 | mythical-requester | tempo | SPAN_KIND_CLIENT | log_to_loki | STATUS_CODE_OK | 43cd491719146ad9c706d73b4870512c | +// | 2022-06-01 12:29:06.809 +0000 UTC | 0.070504 | traces_spanmetrics_duration_seconds_bucket | mythical | | | f1c32fe505c3 | 0.128 | mythical-requester | tempo | SPAN_KIND_CLIENT | requester | STATUS_CODE_OK | e08c737b2dc96959a8dda2cb76c91de7 | +// | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | +// +-----------------------------------+-----------------+--------------------------------------------+----------------+-------------------+-------------------+----------------+----------------+--------------------+----------------+------------------+-----------------+-------------------+----------------------------------+ +// +// // 🌟 This was machine generated. Do not edit. 🌟 { "status": 200, @@ -1046,6 +1076,1020 @@ ] ] } + }, + { + "schema": { + "name": "exemplar", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "custom": { + "resultType": "exemplar" + } + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "__name__", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "group", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "http_method", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "http_target", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "instance", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "le", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "service", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "source", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "span_kind", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "span_name", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "span_status", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "traceID", + "type": "string", + "typeInfo": { + "frame": "string" + } + } + ] + }, + "data": { + "values": [ + [ + 1654086511809, + 1654086516810, + 1654086516810, + 1654086516810, + 1654086521809, + 1654086526809, + 1654086526809, + 1654086536809, + 1654086546809, + 1654086546809, + 1654086546809, + 1654086561810, + 1654086566809, + 1654086566809, + 1654086571809, + 1654086576810, + 1654086576810, + 1654086586809, + 1654086591809, + 1654086591809, + 1654086601809, + 1654086606809, + 1654086616810, + 1654086621809, + 1654086626810, + 1654086631809, + 1654086636809, + 1654086641809, + 1654086646809, + 1654086651809, + 1654086651809, + 1654086661809, + 1654086666809, + 1654086666809, + 1654086676809, + 1654086676809, + 1654086686809, + 1654086696809, + 1654086701809, + 1654086701809, + 1654086706809, + 1654086716809, + 1654086716809, + 1654086721809, + 1654086721809, + 1654086731809, + 1654086731809, + 1654086741809, + 1654086741809, + 1654086746809, + 1654086751809, + 1654086761809, + 1654086761809, + 1654086766809, + 1654086766809, + 1654086776809, + 1654086786809, + 1654086786809, + 1654086791809, + 1654086796809, + 1654086801809, + 1654086801809 + ], + [ + 0.001162, + 0.134971, + 0.090381, + 0.029188, + 0.05719, + 0.071953, + 0.041812, + 0.013244, + 0.070504, + 0.038753, + 0.010491, + 0.01785, + 0.074616, + 0.046192, + 0.067413, + 0.038985, + 0.010387, + 0.055778, + 0.090068, + 0.026474, + 0.020916, + 0.051105, + 0.010103, + 0.038904, + 0.069116, + 0.036012, + 0.06704, + 0.007759, + 0.046809, + 0.076668, + 0.01864, + 0.037142, + 0.066299, + 0.008877, + 0.072569, + 0.038946, + 0.010457, + 0.021391, + 0.089991, + 0.049446, + 0.011579, + 0.068599, + 0.039792, + 0.067805, + 0.010759, + 0.099291, + 0.039441, + 0.06673, + 0.03671, + 0.008554, + 0.039395, + 0.071128, + 0.010549, + 0.043282, + 0.014027, + 0.072983, + 0.070787, + 0.009928, + 0.037937, + 0.010333, + 0.067877, + 0.038846 + ], + [ + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket", + "traces_spanmetrics_duration_seconds_bucket" + ], + [ + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical", + "mythical" + ], + [ + "", + "", + "POST", + "GET", + "", + "", + "GET", + "", + "", + "GET", + "", + "", + "", + "", + "", + "GET", + "", + "", + "", + "GET", + "", + "", + "", + "GET", + "", + "GET", + "", + "", + "GET", + "", + "", + "", + "", + "", + "", + "POST", + "", + "POST", + "", + "", + "POST", + "", + "GET", + "", + "", + "", + "", + "", + "GET", + "POST", + "GET", + "", + "", + "POST", + "", + "", + "", + "", + "GET", + "", + "", + "" + ], + [ + "", + "", + "/loki/api/v1/push", + "/manticore", + "", + "", + "/unicorn", + "", + "", + "/manticore", + "", + "", + "", + "", + "", + "/manticore", + "", + "", + "", + "/manticore", + "", + "", + "", + "/manticore", + "", + "/unicorn", + "", + "", + "/unicorn", + "", + "", + "", + "", + "", + "", + "/loki/api/v1/push", + "", + "/loki/api/v1/push", + "", + "", + "/beholder", + "", + "/manticore", + "", + "", + "", + "", + "", + "/manticore", + "/loki/api/v1/push", + "/manticore", + "", + "", + "/loki/api/v1/push", + "", + "", + "", + "", + "/manticore", + "", + "", + "" + ], + [ + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3", + "f1c32fe505c3" + ], + [ + "0.002", + "0.256", + "0.128", + "0.032", + "0.064", + "0.128", + "0.064", + "0.016", + "0.128", + "0.064", + "0.016", + "0.032", + "0.128", + "0.064", + "0.128", + "0.064", + "0.016", + "0.064", + "0.128", + "0.032", + "0.032", + "0.064", + "0.016", + "0.064", + "0.128", + "0.064", + "0.128", + "0.008", + "0.064", + "0.128", + "0.032", + "0.064", + "0.128", + "0.016", + "0.128", + "0.064", + "0.016", + "0.032", + "0.128", + "0.064", + "0.016", + "0.128", + "0.064", + "0.128", + "0.016", + "0.128", + "0.064", + "0.128", + "0.064", + "0.016", + "0.064", + "0.128", + "0.016", + "0.064", + "0.016", + "0.128", + "0.128", + "0.016", + "0.064", + "0.016", + "0.128", + "0.064" + ], + [ + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-server", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-server", + "mythical-server", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-requester", + "mythical-requester", + "mythical-requester", + "mythical-server", + "mythical-server", + "mythical-server", + "mythical-requester", + "mythical-server" + ], + [ + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo", + "tempo" + ], + [ + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_UNSPECIFIED", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_SERVER", + "SPAN_KIND_CLIENT", + "SPAN_KIND_CLIENT", + "SPAN_KIND_UNSPECIFIED" + ], + [ + "pg.query:INSERT", + "requester", + "HTTP POST", + "GET /:endpoint", + "requester", + "requester", + "GET /:endpoint", + "log_to_loki", + "requester", + "GET /:endpoint", + "pg.query:INSERT", + "log_to_loki", + "requester", + "dns.lookup", + "requester", + "GET /:endpoint", + "pg.query:DELETE", + "requester", + "requester", + "GET /:endpoint", + "requester", + "requester", + "log_to_loki", + "GET /:endpoint", + "requester", + "GET /:endpoint", + "requester", + "log_to_loki", + "GET /:endpoint", + "requester", + "dns.lookup", + "pg.query:INSERT", + "requester", + "tcp.connect", + "requester", + "HTTP POST", + "pg.query:INSERT", + "HTTP POST", + "requester", + "requester", + "POST /:endpoint", + "requester", + "GET /:endpoint", + "requester", + "requester", + "requester", + "pg.query:SELECT", + "requester", + "GET /:endpoint", + "HTTP POST", + "GET /:endpoint", + "requester", + "requester", + "HTTP POST", + "log_to_loki", + "requester", + "requester", + "pg.query:SELECT", + "GET /:endpoint", + "pg.query:DELETE", + "requester", + "tcp.connect" + ], + [ + "STATUS_CODE_ERROR", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET", + "STATUS_CODE_OK", + "STATUS_CODE_UNSET" + ], + [ + "94809f37ec6cc984f2eb2b14a9f8036e", + "46e15ae41852149068aeb26c64643c29", + "46e15ae41852149068aeb26c64643c29", + "1e58dc2083eceb454846f9ab343a79ff", + "d6ff032e85e6e5719a1f6b923042765f", + "b12280a11924d98f7001f9b03779c6bc", + "b12280a11924d98f7001f9b03779c6bc", + "43cd491719146ad9c706d73b4870512c", + "e08c737b2dc96959a8dda2cb76c91de7", + "654d578991bcc53c7de88912a8660488", + "8dec2e77ca64f1cc665afc43b3b1623f", + "e5f765faeb401c335355e0bb79e33b83", + "68d98aae947863f11a7863cd92421038", + "61386de633427db6bb2e5fd216518b3c", + "a17f9f61386f57e7ef85acedc120f82", + "3b4e33abe16c115448759924138cc775", + "ccfb65171f9957871c792f9e7ba4caf0", + "62110ace7888d4e116a0d6182e3ef880", + "1c5d32e0675734a7d86c6146cfef54e1", + "ee885d6d0fa1949978667532d7b50b40", + "45128087f18fb412c24394ed10826446", + "3855df04789862c55b23ca3726fd700a", + "ff6a99aa5fd60f6fbcf0f838164f87ea", + "ca2f23783ee1c390e446f02f7a27cf4", + "2699255777b784b20c8e42bb7889f607", + "72c7ff318a7bb17b6e38aa0f4e94c955", + "78b31a3856158fb296920170f15b2663", + "e81a28f6ee9b468cdac757b825b17623", + "a5277cb54e46140aa3ae489109e1ebc8", + "9ed79aa36a4c6b4c53c7f54e77e6489c", + "9ed79aa36a4c6b4c53c7f54e77e6489c", + "5ec0bc7cc644f654c04190b1cc16d39", + "2d6bde392e5b69cb55588219ce605cbc", + "7c42490bd4361a83bb8ee827b89387ae", + "1b9024f2c2042d52b82d22c36597ec3f", + "1b9024f2c2042d52b82d22c36597ec3f", + "da0d4ef26ef65b5d9c446a51b8662895", + "b1a28a72cc2a4991b2cec793530d605", + "c642d0d7df516be0390080402991b79f", + "a05d1517c61ea0c4ab76b39ab645c9b6", + "2dc68b746bbd6c6958f25c3dbb1ea110", + "ec7dc9dd4096d36ebf564427bf30abc8", + "da805bfda5683c80b9a5538c17346217", + "3835f629b97ac7869de9a839b1e2bf2a", + "1f2e6add2566d120c876e5e4f4602432", + "efa3c8aa7380ae77e734f6f655d9c782", + "312e6ef8f9d9b4ffbb4470e772f43660", + "135989a5bb785b94d339caf672590634", + "a58a30a5caee0a451e7b1ae24bc1e33e", + "d4c096aa222e1016b67de881f80978d8", + "3eefbe3d5fb80a96325688a69c5597e3", + "5384591d9cd576d73f86cbe1803d2a46", + "4f1660168b2dcf5a40474d08a587e8dd", + "bd11e06766d5a045d69b201a9c442258", + "60d05233bc4ccb745055ef19af359686", + "a3374ca45cd256cc5bdb6f482c0a2856", + "a30c5021f274710636e84eca64343b4", + "54b4e0377ac7c101278b68134853562a", + "32dfe6e357ffbb98e7148104e2d660a4", + "a11a727138a3de7ca030687340bd19b0", + "a11c1d3d300f07b1e5d688e96069f76b", + "83bf586cc62842c3187106bb40a6793d" + ] + ] + } } ] } \ No newline at end of file diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go index 1adbcac2440..b4135294e53 100644 --- a/pkg/registry/apis/alerting/notifications/register.go +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -44,7 +44,7 @@ func RegisterAPIService( cfg *setting.Cfg, ng *ngalert.AlertNG, ) *NotificationsAPIBuilder { - if ng.IsDisabled() || !features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { + if ng.IsDisabled() { return nil } builder := &NotificationsAPIBuilder{ diff --git a/pkg/registry/apis/alerting/notifications/routingtree/conversions.go b/pkg/registry/apis/alerting/notifications/routingtree/conversions.go index 7d7d16de825..22aab7593d1 100644 --- a/pkg/registry/apis/alerting/notifications/routingtree/conversions.go +++ b/pkg/registry/apis/alerting/notifications/routingtree/conversions.go @@ -19,7 +19,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func convertToK8sResource(orgID int64, r definitions.Route, version string, namespacer request.NamespaceMapper) (*model.RoutingTree, error) { +func ConvertToK8sResource(orgID int64, r definitions.Route, version string, namespacer request.NamespaceMapper) (*model.RoutingTree, error) { spec := model.Spec{ Defaults: model.RouteDefaults{ GroupBy: r.GroupByStr, diff --git a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go index 07f5138aca8..48ca78c908a 100644 --- a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go @@ -65,7 +65,7 @@ func (s *legacyStorage) getUserDefinedRoutingTree(ctx context.Context) (*model.R if err != nil { return nil, err } - return convertToK8sResource(orgId, res, version, s.namespacer) + return ConvertToK8sResource(orgId, res, version, s.namespacer) } func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) { @@ -131,7 +131,7 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up return nil, false, err } - obj, err = convertToK8sResource(info.OrgID, updated, updatedVersion, s.namespacer) + obj, err = ConvertToK8sResource(info.OrgID, updated, updatedVersion, s.namespacer) return obj, false, err } diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/conversions.go b/pkg/registry/apis/alerting/notifications/timeinterval/conversions.go index 2495033040b..7d4e68f399e 100644 --- a/pkg/registry/apis/alerting/notifications/timeinterval/conversions.go +++ b/pkg/registry/apis/alerting/notifications/timeinterval/conversions.go @@ -15,7 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/provisioning" ) -func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper, selector fields.Selector) (*model.TimeIntervalList, error) { +func ConvertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper, selector fields.Selector) (*model.TimeIntervalList, error) { data, err := json.Marshal(intervals) if err != nil { return nil, err @@ -39,7 +39,7 @@ func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval return result, nil } -func convertToK8sResource(orgID int64, interval definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeInterval, error) { +func ConvertToK8sResource(orgID int64, interval definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeInterval, error) { data, err := json.Marshal(interval) if err != nil { return nil, err diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go index 4e6e990cf90..0134790e81f 100644 --- a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go @@ -67,7 +67,7 @@ func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOpti return nil, err } - return convertToK8sResources(orgId, res, s.namespacer, opts.FieldSelector) + return ConvertToK8sResources(orgId, res, s.namespacer, opts.FieldSelector) } func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) { @@ -83,7 +83,7 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption for _, mt := range timings { if mt.UID == uid { - return convertToK8sResource(info.OrgID, mt, s.namespacer) + return ConvertToK8sResource(info.OrgID, mt, s.namespacer) } } return nil, errors.NewNotFound(ResourceInfo.GroupResource(), uid) @@ -118,7 +118,7 @@ func (s *legacyStorage) Create(ctx context.Context, if err != nil { return nil, err } - return convertToK8sResource(info.OrgID, out, s.namespacer) + return ConvertToK8sResource(info.OrgID, out, s.namespacer) } func (s *legacyStorage) Update(ctx context.Context, @@ -165,7 +165,7 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, false, err } - r, err := convertToK8sResource(info.OrgID, updated, s.namespacer) + r, err := ConvertToK8sResource(info.OrgID, updated, s.namespacer) return r, false, err } diff --git a/pkg/registry/apis/dashboard/large_test.go b/pkg/registry/apis/dashboard/large_test.go index 58876d72f96..c9652bf2b59 100644 --- a/pkg/registry/apis/dashboard/large_test.go +++ b/pkg/registry/apis/dashboard/large_test.go @@ -50,7 +50,7 @@ func TestLargeDashboardSupport(t *testing.T) { small, err := json.MarshalIndent(&dash.Spec, "", " ") require.NoError(t, err) require.JSONEq(t, `{ - "schemaVersion": 33, + "schemaVersion": 36, "title": "Panel tests - All panels", "tags": ["gdev","panel-tests","all-panels"] }`, string(small)) diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go index 67b7336900c..7bf71cddde9 100644 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -222,7 +222,9 @@ func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64, // Now send each dashboard for i := 1; rows.Next(); i++ { dash := rows.row.Dash - dash.APIVersion = fmt.Sprintf("%s/v0alpha1", dashboard.GROUP) // << eventually v0 + if dash.APIVersion == "" { + dash.APIVersion = fmt.Sprintf("%s/v0alpha1", dashboard.GROUP) + } dash.SetNamespace(opts.Namespace) dash.SetResourceVersion("") // it will be filled in by the backend diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index bffdb6a1398..e7aa63cac61 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -15,11 +15,14 @@ import ( claims "github.com/grafana/authlib/types" dashboardOG "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" + dashboardv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" @@ -62,6 +65,7 @@ type dashboardSqlAccess struct { // Typically one... the server wrapper subscribers []chan *resource.WrittenEvent mutex sync.Mutex + log log.Logger } func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, @@ -77,6 +81,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, dashStore: dashStore, provisioning: provisioning, dashboardSearchClient: *dashboardSearchClient, + log: log.New("dashboard.legacysql"), } } @@ -410,6 +415,15 @@ func (a *dashboardSqlAccess) buildSaveDashboardCommand(ctx context.Context, orgI } } + // v1 should be saved as schema version 41. v0 allows for older versions + if strings.HasSuffix(dash.APIVersion, "v1alpha1") { + schemaVersion := schemaversion.GetSchemaVersion(dash.Spec.Object) + if schemaVersion < int(schemaversion.LATEST_VERSION) { + dash.APIVersion = dashboardv0.VERSION + a.log.Info("Downgrading v1alpha1 dashboard to v0alpha1 due to schema version mismatch", "dashboard", dash.Name, "schema_version", schemaVersion) + } + } + apiVersion := strings.TrimPrefix(dash.APIVersion, dashboard.GROUP+"/") meta, err := utils.MetaAccessor(dash) if err != nil { diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index a3accabadad..c3ee67f4fb8 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -14,6 +14,7 @@ import ( common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/user" @@ -30,6 +31,7 @@ func TestScanRow(t *testing.T) { store := &dashboardSqlAccess{ namespacer: func(_ int64) string { return "default" }, provisioning: provisioner, + log: log.New("test"), } columns := []string{"orgId", "dashboard_id", "name", "folder_uid", "deleted", "plugin_id", "origin_name", "origin_path", "origin_hash", "origin_ts", "created", "createdBy", "createdByID", "updated", "updatedBy", "updatedByID", "version", "message", "data", "api_version"} @@ -126,60 +128,95 @@ func TestScanRow(t *testing.T) { } func TestBuildSaveDashboardCommand(t *testing.T) { - mockStore := &dashboards.FakeDashboardStore{} - access := &dashboardSqlAccess{ - dashStore: mockStore, + testCases := []struct { + name string + schemaVersion int + expectedAPI string + }{ + { + name: "with schema version 36 should save as v0alpha1", + schemaVersion: 36, + expectedAPI: "v0alpha1", + }, + { + name: "with schema version 41 should save as v1alpha1", + schemaVersion: 41, + expectedAPI: "v1alpha1", + }, + { + name: "with empty schema version should save as v0alpha1", + schemaVersion: 0, + expectedAPI: "v0alpha1", + }, } - dash := &dashboard.Dashboard{ - TypeMeta: metav1.TypeMeta{ - APIVersion: dashboard.APIVERSION, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-dash", - }, - Spec: common.Unstructured{ - Object: map[string]interface{}{ + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockStore := &dashboards.FakeDashboardStore{} + access := &dashboardSqlAccess{ + dashStore: mockStore, + log: log.New("test"), + } + + dashSpec := map[string]interface{}{ "title": "Test Dashboard", "id": 123, - }, - }, + } + + if tc.schemaVersion > 0 { + dashSpec["schemaVersion"] = tc.schemaVersion + } + + dash := &dashboard.Dashboard{ + TypeMeta: metav1.TypeMeta{ + APIVersion: dashboard.APIVERSION, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-dash", + }, + Spec: common.Unstructured{ + Object: dashSpec, + }, + } + + // fail if no user in context + _, _, err := access.buildSaveDashboardCommand(context.Background(), 1, dash) + require.Error(t, err) + + ctx := identity.WithRequester(context.Background(), &user.SignedInUser{ + OrgID: 1, + OrgRole: "Admin", + }) + // create new dashboard + mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return(nil, nil).Once() + cmd, created, err := access.buildSaveDashboardCommand(ctx, 1, dash) + require.NoError(t, err) + require.Equal(t, true, created) + require.NotNil(t, cmd) + require.Equal(t, "test-dash", cmd.Dashboard.Get("uid").MustString()) + _, exists := cmd.Dashboard.CheckGet("id") + require.False(t, exists) // id should be removed + require.Equal(t, cmd.OrgID, int64(1)) + require.True(t, cmd.Overwrite) + require.Equal(t, tc.expectedAPI, cmd.APIVersion) // verify expected API version + + // now update existing dashboard + mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return( + &dashboards.Dashboard{ + ID: 1234, + Version: 2, + APIVersion: dashboard.APIVERSION, + }, nil).Once() + cmd, created, err = access.buildSaveDashboardCommand(ctx, 1, dash) + require.NoError(t, err) + require.Equal(t, false, created) + require.NotNil(t, cmd) + require.Equal(t, "test-dash", cmd.Dashboard.Get("uid").MustString()) + require.Equal(t, cmd.Dashboard.Get("id").MustInt64(), int64(1234)) // should set to existing ID + require.Equal(t, cmd.Dashboard.Get("version").MustFloat64(), float64(2)) // version must be set - otherwise seen as a new dashboard in NewDashboardFromJson + require.Equal(t, tc.expectedAPI, cmd.APIVersion) // verify expected API version + require.Equal(t, cmd.OrgID, int64(1)) + require.True(t, cmd.Overwrite) + }) } - - // fail if no user in context - _, _, err := access.buildSaveDashboardCommand(context.Background(), 1, dash) - require.Error(t, err) - - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{ - OrgID: 1, - OrgRole: "Admin", - }) - // create new dashboard - mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return(nil, nil).Once() - cmd, created, err := access.buildSaveDashboardCommand(ctx, 1, dash) - require.NoError(t, err) - require.Equal(t, true, created) - require.NotNil(t, cmd) - require.Equal(t, "test-dash", cmd.Dashboard.Get("uid").MustString()) - _, exists := cmd.Dashboard.CheckGet("id") - require.False(t, exists) // id should be removed - require.Equal(t, cmd.OrgID, int64(1)) - require.True(t, cmd.Overwrite) - - // now update existing dashboard - mockStore.On("GetDashboard", mock.Anything, mock.Anything).Return( - &dashboards.Dashboard{ - ID: 1234, - Version: 2, - APIVersion: dashboard.APIVERSION, - }, nil).Once() - cmd, created, err = access.buildSaveDashboardCommand(ctx, 1, dash) - require.NoError(t, err) - require.Equal(t, false, created) - require.NotNil(t, cmd) - require.Equal(t, "test-dash", cmd.Dashboard.Get("uid").MustString()) - require.Equal(t, cmd.Dashboard.Get("id").MustInt64(), int64(1234)) // should set to existing ID - require.Equal(t, cmd.Dashboard.Get("version").MustFloat64(), float64(2)) // version must be set - otherwise seen as a new dashboard in NewDashboardFromJson - require.Equal(t, cmd.APIVersion, "v1alpha1") // should trim prefix - require.Equal(t, cmd.OrgID, int64(1)) - require.True(t, cmd.Overwrite) } diff --git a/pkg/registry/apis/dashboard/legacy/storage_test.go b/pkg/registry/apis/dashboard/legacy/storage_test.go index 37de58c5c75..a4632aba459 100644 --- a/pkg/registry/apis/dashboard/legacy/storage_test.go +++ b/pkg/registry/apis/dashboard/legacy/storage_test.go @@ -12,6 +12,7 @@ import ( dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -119,6 +120,7 @@ func TestWriteProvisioningEvent(t *testing.T) { access := &dashboardSqlAccess{ dashStore: mockStore, + log: log.New("test"), } ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) diff --git a/pkg/registry/apis/dashboard/mutate.go b/pkg/registry/apis/dashboard/mutate.go index d91ea8fe679..e2d0a02d116 100644 --- a/pkg/registry/apis/dashboard/mutate.go +++ b/pkg/registry/apis/dashboard/mutate.go @@ -12,6 +12,9 @@ import ( "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "github.com/grafana/grafana/pkg/apimachinery/utils" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" ) func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { @@ -28,6 +31,8 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute return err } + var migrationErr error + var resourceInfo utils.ResourceInfo switch v := obj.(type) { case *dashboardV0.Dashboard: delete(v.Spec.Object, "uid") @@ -36,6 +41,7 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute delete(v.Spec.Object, "id") internalID = int64(id) } + resourceInfo = dashboardV0.DashboardResourceInfo case *dashboardV1.Dashboard: delete(v.Spec.Object, "uid") delete(v.Spec.Object, "version") @@ -43,15 +49,26 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute delete(v.Spec.Object, "id") internalID = int64(id) } - // do not error here if the migrations fail - err = migration.Migrate(v.Spec.Object, schemaversion.LATEST_VERSION) - if err != nil { + resourceInfo = dashboardV1.DashboardResourceInfo + migrationErr = migration.Migrate(v.Spec.Object, schemaversion.LATEST_VERSION) + if migrationErr != nil { v.Status.Conversion = &dashboardV1.DashboardConversionStatus{ Failed: true, - Error: err.Error(), + Error: migrationErr.Error(), } } case *dashboardV2.Dashboard: + // Temporary fix: The generator fails to properly initialize this property, so we'll do it here + // until the generator is fixed. + if v.Spec.Layout.GridLayoutKind == nil && v.Spec.Layout.RowsLayoutKind == nil && v.Spec.Layout.AutoGridLayoutKind == nil && v.Spec.Layout.TabsLayoutKind == nil { + v.Spec.Layout.GridLayoutKind = &dashboardV2.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashboardV2.DashboardGridLayoutSpec{}, + } + } + + resourceInfo = dashboardV2.DashboardResourceInfo + // Noop for V2 default: return fmt.Errorf("mutation error: expected to dashboard, got %T", obj) @@ -61,5 +78,46 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute meta.SetDeprecatedInternalID(internalID) // nolint:staticcheck } + fieldValidationMode := getFieldValidationMode(a) + + var validationErrorList field.ErrorList + var validationProcessingError error + if migrationErr == nil { + // Migration check passed, validate the spec now - this will respect the field validation mode! + validationErrorList, validationProcessingError = b.ValidateDashboardSpec(ctx, obj, fieldValidationMode) + } + + // Only fail if the field validation mode is strict + if fieldValidationMode == metav1.FieldValidationStrict { + if migrationErr != nil { + return apierrors.NewInvalid(resourceInfo.GroupVersionKind().GroupKind(), meta.GetName(), field.ErrorList{ + field.Invalid(field.NewPath("spec"), meta.GetName(), migrationErr.Error())}) + } + if validationProcessingError != nil { + return validationProcessingError + } + if len(validationErrorList) > 0 { + return apierrors.NewInvalid(resourceInfo.GroupVersionKind().GroupKind(), meta.GetName(), validationErrorList) + } + } + return nil } + +func getFieldValidationMode(a admission.Attributes) string { + var validation string + switch opts := a.GetOperationOptions().(type) { + case *metav1.CreateOptions: + validation = opts.FieldValidation + case *metav1.UpdateOptions: + validation = opts.FieldValidation + default: + validation = metav1.FieldValidationStrict + } + + if validation == "" { + validation = metav1.FieldValidationStrict + } + + return validation +} diff --git a/pkg/registry/apis/dashboard/mutation_test.go b/pkg/registry/apis/dashboard/mutation_test.go index 6e702edde48..9865fb1828d 100644 --- a/pkg/registry/apis/dashboard/mutation_test.go +++ b/pkg/registry/apis/dashboard/mutation_test.go @@ -6,10 +6,13 @@ import ( dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" @@ -17,11 +20,14 @@ import ( func TestDashboardAPIBuilder_Mutate(t *testing.T) { tests := []struct { - name string - inputObj runtime.Object - operation admission.Operation - expectedID int64 - migrationExpected bool + name string + inputObj runtime.Object + operation admission.Operation + expectedID int64 + migrationExpected bool + expectedTitle string + expectedError bool + fieldValidationMode string }{ { name: "should skip non-create/update operations", @@ -47,6 +53,47 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { operation: admission.Create, expectedID: 123, }, + { + name: "v0 should not fail with invalid schema", + inputObj: &dashv0.Dashboard{ + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "id": float64(123), + "revision": "revision-is-a-number", + }, + }, + }, + operation: admission.Create, + expectedID: 123, + }, + { + name: "v1 should fail with invalid schema", + inputObj: &dashv1.Dashboard{ + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "id": float64(123), + "revision": "revision-is-a-number", + }, + }, + }, + operation: admission.Create, + expectedError: true, + }, + { + name: "v1 should not fail with invalid schema and FieldValidationIgnore is set", + inputObj: &dashv1.Dashboard{ + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "id": float64(123), + "revision": "revision-is-a-number", + }, + }, + }, + operation: admission.Create, + fieldValidationMode: metav1.FieldValidationIgnore, + expectedError: false, + expectedID: 123, + }, { name: "v1 should migrate dashboard to the latest version, if possible, and set as label", inputObj: &dashv1.Dashboard{ @@ -62,7 +109,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { migrationExpected: true, }, { - name: "v1 should not error mutation hook if migration fails", + name: "v1 should error mutation hook if migration fails", inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{ Object: map[string]interface{}{ @@ -71,14 +118,50 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { }, }, }, - operation: admission.Create, - expectedID: 456, + operation: admission.Create, + expectedError: true, + }, + { + name: "v1 should not error mutation hook if migration fails and FieldValidationIgnore is set", + inputObj: &dashv1.Dashboard{ + Spec: common.Unstructured{ + Object: map[string]interface{}{ + "id": float64(456), + "schemaVersion": schemaversion.MIN_VERSION - 1, + }, + }, + }, + expectedID: 456, + operation: admission.Create, + fieldValidationMode: metav1.FieldValidationIgnore, + expectedError: false, + }, + { + name: "v2 should set layout if it is not set", + inputObj: &v2alpha1.Dashboard{ + Spec: v2alpha1.DashboardSpec{ + Title: "test123", + }, + }, + operation: admission.Create, + expectedTitle: "test123", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := &DashboardsAPIBuilder{} + b := &DashboardsAPIBuilder{ + features: featuremgmt.WithFeatures(), + } + var operationOptions runtime.Object + switch tt.operation { + case admission.Create: + operationOptions = &metav1.CreateOptions{FieldValidation: tt.fieldValidationMode} + case admission.Update: + operationOptions = &metav1.UpdateOptions{FieldValidation: tt.fieldValidationMode} + default: + operationOptions = nil + } err := b.Mutate(context.Background(), admission.NewAttributesRecord( tt.inputObj, nil, @@ -88,10 +171,15 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { schema.GroupVersionResource{}, "", tt.operation, - nil, + operationOptions, false, nil, ), nil) + + if tt.expectedError { + require.Error(t, err) + return + } require.NoError(t, err) if tt.operation == admission.Create || tt.operation == admission.Update { @@ -111,6 +199,10 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { if tt.migrationExpected { require.Equal(t, schemaversion.LATEST_VERSION, schemaVersion, "dashboard should be migrated to the latest version") } + case *v2alpha1.Dashboard: + require.Equal(t, tt.expectedTitle, v.Spec.Title, "title should be set") + require.NotNil(t, v.Spec.Layout, "layout should be set") + require.NotNil(t, v.Spec.Layout.GridLayoutKind, "layout should be a GridLayout") } } }) diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index 82a5ed934f1..51d0ab4ef43 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -254,12 +254,12 @@ func (b *DashboardsAPIBuilder) validateCreate(ctx context.Context, a admission.A // Basic validations if err := b.dashboardService.ValidateBasicDashboardProperties(title, accessor.GetName(), accessor.GetMessage()); err != nil { - return err + return apierrors.NewBadRequest(err.Error()) } // Validate refresh interval if err := b.dashboardService.ValidateDashboardRefreshInterval(b.cfg.MinRefreshInterval, refresh); err != nil { - return err + return apierrors.NewBadRequest(err.Error()) } id, err := identity.GetRequester(ctx) @@ -270,7 +270,7 @@ func (b *DashboardsAPIBuilder) validateCreate(ctx context.Context, a admission.A // Validate folder existence if specified if !a.IsDryRun() && accessor.GetFolder() != "" { if err := b.validateFolderExists(ctx, accessor.GetFolder(), id.GetOrgID()); err != nil { - return err + return apierrors.NewNotFound(folders.FolderResourceInfo.GroupResource(), accessor.GetFolder()) } } @@ -288,7 +288,7 @@ func (b *DashboardsAPIBuilder) validateCreate(ctx context.Context, a admission.A return err } if quotaReached { - return dashboards.ErrQuotaReached + return apierrors.NewForbidden(dashv1.DashboardResourceInfo.GroupResource(), a.GetName(), dashboards.ErrQuotaReached) } } @@ -324,19 +324,19 @@ func (b *DashboardsAPIBuilder) validateUpdate(ctx context.Context, a admission.A // Basic validations if err := b.dashboardService.ValidateBasicDashboardProperties(title, newAccessor.GetName(), newAccessor.GetMessage()); err != nil { - return err + return apierrors.NewBadRequest(err.Error()) } // Validate folder existence if specified and changed if !a.IsDryRun() && newAccessor.GetFolder() != "" && newAccessor.GetFolder() != oldAccessor.GetFolder() { if err := b.validateFolderExists(ctx, newAccessor.GetFolder(), nsInfo.OrgID); err != nil { - return err + return apierrors.NewNotFound(folders.FolderResourceInfo.GroupResource(), newAccessor.GetFolder()) } } // Validate refresh interval if err := b.dashboardService.ValidateDashboardRefreshInterval(b.cfg.MinRefreshInterval, refresh); err != nil { - return err + return apierrors.NewBadRequest(err.Error()) } allowOverwrite := false // TODO: Add support for overwrite flag @@ -345,7 +345,7 @@ func (b *DashboardsAPIBuilder) validateUpdate(ctx context.Context, a admission.A if allowOverwrite { newAccessor.SetGeneration(oldAccessor.GetGeneration()) } else { - return dashboards.ErrDashboardVersionMismatch + return apierrors.NewBadRequest(dashboards.ErrDashboardVersionMismatch.Error()) } } diff --git a/pkg/registry/apis/dashboard/schema_validation.go b/pkg/registry/apis/dashboard/schema_validation.go new file mode 100644 index 00000000000..7f09de98928 --- /dev/null +++ b/pkg/registry/apis/dashboard/schema_validation.go @@ -0,0 +1,73 @@ +package dashboard + +import ( + "context" + _ "embed" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/featuremgmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ValidateDashboardSpec validates the dashboard spec and throws a detailed error if there are validation errors. +func (b *DashboardsAPIBuilder) ValidateDashboardSpec(ctx context.Context, obj runtime.Object, fieldValidationMode string) (field.ErrorList, error) { + accessor, err := utils.MetaAccessor(obj) + if err != nil { + return nil, fmt.Errorf("error getting meta accessor: %w", err) + } + + errorOnSchemaMismatches := false + mode := fieldValidationMode + if mode != metav1.FieldValidationIgnore { + switch obj.(type) { + case *v0alpha1.Dashboard: + errorOnSchemaMismatches = false // Never error for v0 + case *v1alpha1.Dashboard: + errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV1) + case *v2alpha1.Dashboard: + errorOnSchemaMismatches = !b.features.IsEnabled(ctx, featuremgmt.FlagDashboardDisableSchemaValidationV2) + default: + return nil, fmt.Errorf("invalid dashboard type: %T", obj) + } + } + if mode == metav1.FieldValidationWarn { + return nil, apierrors.NewBadRequest("Not supported: FieldValidationMode: Warn") + } + + alwaysLogSchemaValidationErrors := b.features.IsEnabled(ctx, featuremgmt.FlagDashboardSchemaValidationLogging) + + var errors field.ErrorList + var schemaVersionError field.ErrorList + if errorOnSchemaMismatches || alwaysLogSchemaValidationErrors { + switch v := obj.(type) { + case *v0alpha1.Dashboard: + errors, schemaVersionError = v0alpha1.ValidateDashboardSpec(v, alwaysLogSchemaValidationErrors) + case *v1alpha1.Dashboard: + errors, schemaVersionError = v1alpha1.ValidateDashboardSpec(v, alwaysLogSchemaValidationErrors) + case *v2alpha1.Dashboard: + errors = v2alpha1.ValidateDashboardSpec(v) + } + } + + if alwaysLogSchemaValidationErrors && len(errors) > 0 { + b.log.Info("Schema validation errors during dashboard validation", "group_version", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(), "name", accessor.GetName(), "errors", errors.ToAggregate().Error(), "schema_version_mismatch", schemaVersionError != nil) + } + + if errorOnSchemaMismatches { + if schemaVersionError != nil { + return schemaVersionError, nil + } + if len(errors) > 0 { + return errors, nil + } + } + return nil, nil +} diff --git a/pkg/registry/apis/provisioning/controller/repository.go b/pkg/registry/apis/provisioning/controller/repository.go index 3274ed0928b..f4c7e09d7e9 100644 --- a/pkg/registry/apis/provisioning/controller/repository.go +++ b/pkg/registry/apis/provisioning/controller/repository.go @@ -271,18 +271,22 @@ func (rc *RepositoryController) runHealthCheck(ctx context.Context, repo reposit if err != nil { res = &provisioning.TestResults{ Success: false, - Errors: []string{ - "error running test repository", - err.Error(), - }, + Errors: []provisioning.ErrorDetails{{ + Detail: fmt.Sprintf("error running test repository: %s", err.Error()), + }}, } } healthStatus := provisioning.HealthStatus{ Healthy: res.Success, Checked: time.Now().UnixMilli(), - Message: res.Errors, } + for _, err := range res.Errors { + if err.Detail != "" { + healthStatus.Message = append(healthStatus.Message, err.Detail) + } + } + logger.Info("health check completed", "status", healthStatus) return healthStatus diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/changes.go b/pkg/registry/apis/provisioning/jobs/pullrequest/changes.go index 7946f969789..961485b1eb6 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/changes.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/changes.go @@ -50,54 +50,67 @@ type fileChangeInfo struct { PreviewScreenshotURL string } -type changeOptions struct { - grafanaBaseURL string - pullRequest provisioning.PullRequestJobOptions - changes []repository.VersionedFileChange - parser resources.Parser - reader repository.Reader - progress jobs.JobProgressRecorder - render ScreenshotRenderer // from config +type evaluator struct { + render ScreenshotRenderer + parsers resources.ParserFactory + urlProvider func(namespace string) string +} + +func NewEvaluator(render ScreenshotRenderer, parsers resources.ParserFactory, urlProvider func(namespace string) string) Evaluator { + return &evaluator{ + render: render, + parsers: parsers, + urlProvider: urlProvider, + } } // This will process the list of versioned file changes into changeInfo -func processChangedFiles(ctx context.Context, opts changeOptions) (changeInfo, error) { - info := changeInfo{ - GrafanaBaseURL: opts.grafanaBaseURL, +func (e *evaluator) Evaluate(ctx context.Context, repo repository.Reader, opts provisioning.PullRequestJobOptions, changes []repository.VersionedFileChange, progress jobs.JobProgressRecorder) (changeInfo, error) { + cfg := repo.Config() + parser, err := e.parsers.GetParser(ctx, repo) + if err != nil { + return changeInfo{}, fmt.Errorf("failed to get parser for %s: %w", cfg.Name, err) } - if opts.render != nil { - if !opts.render.IsAvailable(ctx) { - info.MissingImageRenderer = true - opts.render = nil - } + baseURL := e.urlProvider(cfg.Namespace) + info := changeInfo{ + GrafanaBaseURL: baseURL, + } + var shouldRender bool + switch { + case e.render == nil: + shouldRender = false + case !e.render.IsAvailable(ctx): + info.MissingImageRenderer = true + shouldRender = false + case len(changes) > 1 || !cfg.Spec.GitHub.GenerateDashboardPreviews: // Only render images when there is just one change - if len(opts.changes) > 1 { - opts.render = nil - } + shouldRender = false + default: + shouldRender = true } logger := logging.FromContext(ctx) - for i, change := range opts.changes { + for i, change := range changes { // process maximum 10 files if i >= 10 { - info.SkippedFiles = len(opts.changes) - i + info.SkippedFiles = len(changes) - i break } - opts.progress.SetMessage(ctx, fmt.Sprintf("processing: %s", change.Path)) + progress.SetMessage(ctx, fmt.Sprintf("processing: %s", change.Path)) logger.With("action", change.Action).With("path", change.Path) - v, err := calculateFileChangeInfo(ctx, info.GrafanaBaseURL, change, opts) + v, err := calculateFileChangeInfo(ctx, repo, info.GrafanaBaseURL, change, opts, parser) if err != nil { return info, fmt.Errorf("error calculating changes %w", err) } // If everything applied OK, then render screenshots - if opts.render != nil && v.GrafanaURL != "" && v.Parsed != nil && v.Parsed.DryRunResponse != nil { - opts.progress.SetMessage(ctx, fmt.Sprintf("rendering screenshots: %s", change.Path)) - if err = v.renderScreenshots(ctx, info.GrafanaBaseURL, opts.render); err != nil { + if shouldRender && v.GrafanaURL != "" && v.Parsed != nil && v.Parsed.DryRunResponse != nil { + progress.SetMessage(ctx, fmt.Sprintf("rendering screenshots: %s", change.Path)) + if err = v.renderScreenshots(ctx, info.GrafanaBaseURL, e.render); err != nil { info.MissingImageRenderer = true if v.Error == "" { v.Error = "Error running image rendering" @@ -116,13 +129,13 @@ func processChangedFiles(ctx context.Context, opts changeOptions) (changeInfo, e var dashboardKind = dashboard.DashboardResourceInfo.GroupVersionKind().Kind -func calculateFileChangeInfo(ctx context.Context, baseURL string, change repository.VersionedFileChange, opts changeOptions) (fileChangeInfo, error) { +func calculateFileChangeInfo(ctx context.Context, repo repository.Reader, baseURL string, change repository.VersionedFileChange, opts provisioning.PullRequestJobOptions, parser resources.Parser) (fileChangeInfo, error) { if change.Action == repository.FileActionDeleted { - return calculateFileDeleteInfo(ctx, baseURL, change, opts) + return calculateFileDeleteInfo(ctx, baseURL, change) } info := fileChangeInfo{Change: change} - fileInfo, err := opts.reader.Read(ctx, change.Path, change.Ref) + fileInfo, err := repo.Read(ctx, change.Path, change.Ref) if err != nil { logger.Info("unable to read file", "err", err) info.Error = err.Error() @@ -130,7 +143,7 @@ func calculateFileChangeInfo(ctx context.Context, baseURL string, change reposit } // Read the file as a resource - info.Parsed, err = opts.parser.Parse(ctx, fileInfo) + info.Parsed, err = parser.Parse(ctx, fileInfo) if err != nil { info.Error = err.Error() return info, nil @@ -161,8 +174,8 @@ func calculateFileChangeInfo(ctx context.Context, baseURL string, change reposit query := url.Values{} query.Set("ref", info.Parsed.Info.Ref) - if opts.pullRequest.URL != "" { - query.Set("pull_request_url", url.QueryEscape(opts.pullRequest.URL)) + if opts.URL != "" { + query.Set("pull_request_url", url.QueryEscape(opts.URL)) } info.PreviewURL += "?" + query.Encode() } @@ -170,7 +183,7 @@ func calculateFileChangeInfo(ctx context.Context, baseURL string, change reposit return info, nil } -func calculateFileDeleteInfo(_ context.Context, _ string, change repository.VersionedFileChange, opts changeOptions) (fileChangeInfo, error) { +func calculateFileDeleteInfo(_ context.Context, _ string, change repository.VersionedFileChange) (fileChangeInfo, error) { // TODO -- read the old and verify return fileChangeInfo{Change: change, Error: "delete feedback not yet implemented"}, nil } diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go b/pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go index 9f0e04947f0..c45374cc1f4 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go @@ -7,16 +7,16 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" ) func TestCalculateChanges(t *testing.T) { @@ -45,6 +45,17 @@ func TestCalculateChanges(t *testing.T) { progress.On("SetMessage", mock.Anything, mock.Anything).Return() reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil) + reader.On("Config").Return(&v0alpha1.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "x", + }, + Spec: v0alpha1.RepositorySpec{ + GitHub: &v0alpha1.GitHubRepositoryConfig{ + GenerateDashboardPreviews: true, + }, + }, + }) parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{ Info: finfo, Repo: v0alpha1.ResourceRepositoryInfo{ @@ -76,18 +87,15 @@ func TestCalculateChanges(t *testing.T) { renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true) renderer.On("RenderScreenshot", mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(getDummyRenderedURL("x"), nil) + changes := []repository.VersionedFileChange{createdFileChange} - options := changeOptions{ - grafanaBaseURL: "http://host/", - pullRequest: pullRequest, - changes: []repository.VersionedFileChange{createdFileChange}, - parser: parser, - reader: reader, - progress: progress, - render: renderer, - } + parserFactory := resources.NewMockParserFactory(t) + parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil) + evaluator := NewEvaluator(renderer, parserFactory, func(_ string) string { + return "http://host/" + }) - info, err := processChangedFiles(context.Background(), options) + info, err := evaluator.Evaluate(context.Background(), reader, pullRequest, changes, progress) require.NoError(t, err) require.False(t, info.MissingImageRenderer) @@ -107,17 +115,14 @@ func TestCalculateChanges(t *testing.T) { t.Run("without-screenshot", func(t *testing.T) { renderer := NewMockScreenshotRenderer(t) renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false) - options := changeOptions{ - grafanaBaseURL: "http://host/", - pullRequest: pullRequest, - changes: []repository.VersionedFileChange{createdFileChange}, - parser: parser, - reader: reader, - progress: progress, - render: renderer, - } + changes := []repository.VersionedFileChange{createdFileChange} + parserFactory := resources.NewMockParserFactory(t) + parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil) + evaluator := NewEvaluator(renderer, parserFactory, func(_ string) string { + return "http://host/" + }) - info, err := processChangedFiles(context.Background(), options) + info, err := evaluator.Evaluate(context.Background(), reader, pullRequest, changes, progress) require.NoError(t, err) require.True(t, info.MissingImageRenderer) @@ -138,19 +143,18 @@ func TestCalculateChanges(t *testing.T) { renderer := NewMockScreenshotRenderer(t) renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true) - options := changeOptions{ - grafanaBaseURL: "http://host/", - pullRequest: pullRequest, - parser: parser, - reader: reader, - progress: progress, - render: renderer, // not used - } + changes := []repository.VersionedFileChange{} for range 15 { - options.changes = append(options.changes, createdFileChange) + changes = append(changes, createdFileChange) } - info, err := processChangedFiles(context.Background(), options) + parserFactory := resources.NewMockParserFactory(t) + parserFactory.On("GetParser", mock.Anything, mock.Anything).Return(parser, nil) + evaluator := NewEvaluator(renderer, parserFactory, func(_ string) string { + return "http://host/" + }) + + info, err := evaluator.Evaluate(context.Background(), reader, pullRequest, changes, progress) require.NoError(t, err) require.False(t, info.MissingImageRenderer) diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/comment.go b/pkg/registry/apis/provisioning/jobs/pullrequest/comment.go index e7c098af776..12003b73305 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/comment.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/comment.go @@ -9,21 +9,21 @@ import ( "strings" ) -type commentBuilder struct { +type commenter struct { templateDashboard *template.Template templateTable *template.Template templateRenderInfo *template.Template } -func newCommentBuilder() *commentBuilder { - return &commentBuilder{ +func NewCommenter() Commenter { + return &commenter{ templateDashboard: template.Must(template.New("dashboard").Parse(commentTemplateSingleDashboard)), templateTable: template.Must(template.New("table").Parse(commentTemplateTable)), templateRenderInfo: template.Must(template.New("setup").Parse(commentTemplateMissingImageRenderer)), } } -func (c *commentBuilder) Comment(ctx context.Context, prRepo PullRequestRepo, pr int, info changeInfo) error { +func (c *commenter) Comment(ctx context.Context, prRepo PullRequestRepo, pr int, info changeInfo) error { comment, err := c.generateComment(ctx, info) if err != nil { return fmt.Errorf("unable to generate comment text: %w", err) @@ -32,16 +32,16 @@ func (c *commentBuilder) Comment(ctx context.Context, prRepo PullRequestRepo, pr if err := prRepo.CommentPullRequest(ctx, pr, comment); err != nil { return fmt.Errorf("comment pull request: %w", err) } + return nil } -func (c *commentBuilder) generateComment(_ context.Context, info changeInfo) (string, error) { +func (c *commenter) generateComment(_ context.Context, info changeInfo) (string, error) { if len(info.Changes) == 0 { return "no changes found", nil } var buf bytes.Buffer - if len(info.Changes) == 1 && info.Changes[0].Parsed.GVK.Kind == dashboardKind { if err := c.templateDashboard.Execute(&buf, info.Changes[0]); err != nil { return "", fmt.Errorf("unable to execute template: %w", err) @@ -63,7 +63,7 @@ func (c *commentBuilder) generateComment(_ context.Context, info changeInfo) (st const commentTemplateSingleDashboard = `Hey there! 🎉 Grafana spotted some changes to your dashboard. - + {{- if and .GrafanaScreenshotURL .PreviewScreenshotURL}} ### Side by Side Comparison of {{.Parsed.Info.Path}} | Before | After | diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go b/pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go index 9d5271ed0a3..ec0d0da27bf 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" @@ -17,8 +16,6 @@ import ( ) func TestGenerateComment(t *testing.T) { - builder := newCommentBuilder() - for _, tc := range []struct { Name string Input changeInfo @@ -119,27 +116,19 @@ func TestGenerateComment(t *testing.T) { }}, } { t.Run(tc.Name, func(t *testing.T) { - comment, err := builder.generateComment(context.Background(), tc.Input) - require.NoError(t, err) + repo := NewMockPullRequestRepo(t) + // expectation on the comment fpath := filepath.Join("testdata", strings.ReplaceAll(tc.Name, " ", "-")+".md") - update := false - // We can ignore the gosec G304 because this is only for tests // nolint:gosec expect, err := os.ReadFile(fpath) - if err != nil || len(expect) < 1 { - update = true - t.Error("missing " + fpath) - } else { - if diff := cmp.Diff(string(expect), comment); diff != "" { - t.Errorf("%s: %s", fpath, diff) - update = true - } - } - if update { - _ = os.WriteFile(fpath, []byte(comment), 0777) - } + require.NoError(t, err) + repo.On("CommentPullRequest", context.Background(), 1, string(expect)).Return(nil) + + commenter := NewCommenter() + err = commenter.Comment(context.Background(), repo, 1, tc.Input) + require.NoError(t, err) }) } } diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/mock_commenter.go b/pkg/registry/apis/provisioning/jobs/pullrequest/mock_commenter.go new file mode 100644 index 00000000000..785e552f68d --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/mock_commenter.go @@ -0,0 +1,85 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package pullrequest + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockCommenter is an autogenerated mock type for the Commenter type +type MockCommenter struct { + mock.Mock +} + +type MockCommenter_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCommenter) EXPECT() *MockCommenter_Expecter { + return &MockCommenter_Expecter{mock: &_m.Mock} +} + +// Comment provides a mock function with given fields: ctx, repo, pr, changeInfo3 +func (_m *MockCommenter) Comment(ctx context.Context, repo PullRequestRepo, pr int, changeInfo3 changeInfo) error { + ret := _m.Called(ctx, repo, pr, changeInfo3) + + if len(ret) == 0 { + panic("no return value specified for Comment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, PullRequestRepo, int, changeInfo) error); ok { + r0 = rf(ctx, repo, pr, changeInfo3) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockCommenter_Comment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Comment' +type MockCommenter_Comment_Call struct { + *mock.Call +} + +// Comment is a helper method to define mock.On call +// - ctx context.Context +// - repo PullRequestRepo +// - pr int +// - changeInfo3 changeInfo +func (_e *MockCommenter_Expecter) Comment(ctx interface{}, repo interface{}, pr interface{}, changeInfo3 interface{}) *MockCommenter_Comment_Call { + return &MockCommenter_Comment_Call{Call: _e.mock.On("Comment", ctx, repo, pr, changeInfo3)} +} + +func (_c *MockCommenter_Comment_Call) Run(run func(ctx context.Context, repo PullRequestRepo, pr int, changeInfo3 changeInfo)) *MockCommenter_Comment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(PullRequestRepo), args[2].(int), args[3].(changeInfo)) + }) + return _c +} + +func (_c *MockCommenter_Comment_Call) Return(_a0 error) *MockCommenter_Comment_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCommenter_Comment_Call) RunAndReturn(run func(context.Context, PullRequestRepo, int, changeInfo) error) *MockCommenter_Comment_Call { + _c.Call.Return(run) + return _c +} + +// NewMockCommenter creates a new instance of MockCommenter. 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 NewMockCommenter(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCommenter { + mock := &MockCommenter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/mock_evaluator.go b/pkg/registry/apis/provisioning/jobs/pullrequest/mock_evaluator.go new file mode 100644 index 00000000000..54673cd92d8 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/mock_evaluator.go @@ -0,0 +1,101 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package pullrequest + +import ( + context "context" + + jobs "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + mock "github.com/stretchr/testify/mock" + + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +// MockEvaluator is an autogenerated mock type for the Evaluator type +type MockEvaluator struct { + mock.Mock +} + +type MockEvaluator_Expecter struct { + mock *mock.Mock +} + +func (_m *MockEvaluator) EXPECT() *MockEvaluator_Expecter { + return &MockEvaluator_Expecter{mock: &_m.Mock} +} + +// Evaluate provides a mock function with given fields: ctx, repo, opts, changes, progress +func (_m *MockEvaluator) Evaluate(ctx context.Context, repo repository.Reader, opts v0alpha1.PullRequestJobOptions, changes []repository.VersionedFileChange, progress jobs.JobProgressRecorder) (changeInfo, error) { + ret := _m.Called(ctx, repo, opts, changes, progress) + + if len(ret) == 0 { + panic("no return value specified for Evaluate") + } + + var r0 changeInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, repository.Reader, v0alpha1.PullRequestJobOptions, []repository.VersionedFileChange, jobs.JobProgressRecorder) (changeInfo, error)); ok { + return rf(ctx, repo, opts, changes, progress) + } + if rf, ok := ret.Get(0).(func(context.Context, repository.Reader, v0alpha1.PullRequestJobOptions, []repository.VersionedFileChange, jobs.JobProgressRecorder) changeInfo); ok { + r0 = rf(ctx, repo, opts, changes, progress) + } else { + r0 = ret.Get(0).(changeInfo) + } + + if rf, ok := ret.Get(1).(func(context.Context, repository.Reader, v0alpha1.PullRequestJobOptions, []repository.VersionedFileChange, jobs.JobProgressRecorder) error); ok { + r1 = rf(ctx, repo, opts, changes, progress) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockEvaluator_Evaluate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Evaluate' +type MockEvaluator_Evaluate_Call struct { + *mock.Call +} + +// Evaluate is a helper method to define mock.On call +// - ctx context.Context +// - repo repository.Reader +// - opts v0alpha1.PullRequestJobOptions +// - changes []repository.VersionedFileChange +// - progress jobs.JobProgressRecorder +func (_e *MockEvaluator_Expecter) Evaluate(ctx interface{}, repo interface{}, opts interface{}, changes interface{}, progress interface{}) *MockEvaluator_Evaluate_Call { + return &MockEvaluator_Evaluate_Call{Call: _e.mock.On("Evaluate", ctx, repo, opts, changes, progress)} +} + +func (_c *MockEvaluator_Evaluate_Call) Run(run func(ctx context.Context, repo repository.Reader, opts v0alpha1.PullRequestJobOptions, changes []repository.VersionedFileChange, progress jobs.JobProgressRecorder)) *MockEvaluator_Evaluate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(repository.Reader), args[2].(v0alpha1.PullRequestJobOptions), args[3].([]repository.VersionedFileChange), args[4].(jobs.JobProgressRecorder)) + }) + return _c +} + +func (_c *MockEvaluator_Evaluate_Call) Return(_a0 changeInfo, _a1 error) *MockEvaluator_Evaluate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockEvaluator_Evaluate_Call) RunAndReturn(run func(context.Context, repository.Reader, v0alpha1.PullRequestJobOptions, []repository.VersionedFileChange, jobs.JobProgressRecorder) (changeInfo, error)) *MockEvaluator_Evaluate_Call { + _c.Call.Return(run) + return _c +} + +// NewMockEvaluator creates a new instance of MockEvaluator. 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 NewMockEvaluator(t interface { + mock.TestingT + Cleanup(func()) +}) *MockEvaluator { + mock := &MockEvaluator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/mock_pullrequest_repo.go b/pkg/registry/apis/provisioning/jobs/pullrequest/mock_pullrequest_repo.go new file mode 100644 index 00000000000..882ee50cbe9 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/mock_pullrequest_repo.go @@ -0,0 +1,351 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package pullrequest + +import ( + context "context" + + repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + mock "github.com/stretchr/testify/mock" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +// MockPullRequestRepo is an autogenerated mock type for the PullRequestRepo type +type MockPullRequestRepo struct { + mock.Mock +} + +type MockPullRequestRepo_Expecter struct { + mock *mock.Mock +} + +func (_m *MockPullRequestRepo) EXPECT() *MockPullRequestRepo_Expecter { + return &MockPullRequestRepo_Expecter{mock: &_m.Mock} +} + +// ClearAllPullRequestFileComments provides a mock function with given fields: ctx, pr +func (_m *MockPullRequestRepo) ClearAllPullRequestFileComments(ctx context.Context, pr int) error { + ret := _m.Called(ctx, pr) + + if len(ret) == 0 { + panic("no return value specified for ClearAllPullRequestFileComments") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, pr) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPullRequestRepo_ClearAllPullRequestFileComments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClearAllPullRequestFileComments' +type MockPullRequestRepo_ClearAllPullRequestFileComments_Call struct { + *mock.Call +} + +// ClearAllPullRequestFileComments is a helper method to define mock.On call +// - ctx context.Context +// - pr int +func (_e *MockPullRequestRepo_Expecter) ClearAllPullRequestFileComments(ctx interface{}, pr interface{}) *MockPullRequestRepo_ClearAllPullRequestFileComments_Call { + return &MockPullRequestRepo_ClearAllPullRequestFileComments_Call{Call: _e.mock.On("ClearAllPullRequestFileComments", ctx, pr)} +} + +func (_c *MockPullRequestRepo_ClearAllPullRequestFileComments_Call) Run(run func(ctx context.Context, pr int)) *MockPullRequestRepo_ClearAllPullRequestFileComments_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int)) + }) + return _c +} + +func (_c *MockPullRequestRepo_ClearAllPullRequestFileComments_Call) Return(_a0 error) *MockPullRequestRepo_ClearAllPullRequestFileComments_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPullRequestRepo_ClearAllPullRequestFileComments_Call) RunAndReturn(run func(context.Context, int) error) *MockPullRequestRepo_ClearAllPullRequestFileComments_Call { + _c.Call.Return(run) + return _c +} + +// CommentPullRequest provides a mock function with given fields: ctx, pr, comment +func (_m *MockPullRequestRepo) CommentPullRequest(ctx context.Context, pr int, comment string) error { + ret := _m.Called(ctx, pr, comment) + + if len(ret) == 0 { + panic("no return value specified for CommentPullRequest") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int, string) error); ok { + r0 = rf(ctx, pr, comment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPullRequestRepo_CommentPullRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommentPullRequest' +type MockPullRequestRepo_CommentPullRequest_Call struct { + *mock.Call +} + +// CommentPullRequest is a helper method to define mock.On call +// - ctx context.Context +// - pr int +// - comment string +func (_e *MockPullRequestRepo_Expecter) CommentPullRequest(ctx interface{}, pr interface{}, comment interface{}) *MockPullRequestRepo_CommentPullRequest_Call { + return &MockPullRequestRepo_CommentPullRequest_Call{Call: _e.mock.On("CommentPullRequest", ctx, pr, comment)} +} + +func (_c *MockPullRequestRepo_CommentPullRequest_Call) Run(run func(ctx context.Context, pr int, comment string)) *MockPullRequestRepo_CommentPullRequest_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int), args[2].(string)) + }) + return _c +} + +func (_c *MockPullRequestRepo_CommentPullRequest_Call) Return(_a0 error) *MockPullRequestRepo_CommentPullRequest_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPullRequestRepo_CommentPullRequest_Call) RunAndReturn(run func(context.Context, int, string) error) *MockPullRequestRepo_CommentPullRequest_Call { + _c.Call.Return(run) + return _c +} + +// CommentPullRequestFile provides a mock function with given fields: ctx, pr, path, ref, comment +func (_m *MockPullRequestRepo) CommentPullRequestFile(ctx context.Context, pr int, path string, ref string, comment string) error { + ret := _m.Called(ctx, pr, path, ref, comment) + + if len(ret) == 0 { + panic("no return value specified for CommentPullRequestFile") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int, string, string, string) error); ok { + r0 = rf(ctx, pr, path, ref, comment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockPullRequestRepo_CommentPullRequestFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommentPullRequestFile' +type MockPullRequestRepo_CommentPullRequestFile_Call struct { + *mock.Call +} + +// CommentPullRequestFile is a helper method to define mock.On call +// - ctx context.Context +// - pr int +// - path string +// - ref string +// - comment string +func (_e *MockPullRequestRepo_Expecter) CommentPullRequestFile(ctx interface{}, pr interface{}, path interface{}, ref interface{}, comment interface{}) *MockPullRequestRepo_CommentPullRequestFile_Call { + return &MockPullRequestRepo_CommentPullRequestFile_Call{Call: _e.mock.On("CommentPullRequestFile", ctx, pr, path, ref, comment)} +} + +func (_c *MockPullRequestRepo_CommentPullRequestFile_Call) Run(run func(ctx context.Context, pr int, path string, ref string, comment string)) *MockPullRequestRepo_CommentPullRequestFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockPullRequestRepo_CommentPullRequestFile_Call) Return(_a0 error) *MockPullRequestRepo_CommentPullRequestFile_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPullRequestRepo_CommentPullRequestFile_Call) RunAndReturn(run func(context.Context, int, string, string, string) error) *MockPullRequestRepo_CommentPullRequestFile_Call { + _c.Call.Return(run) + return _c +} + +// CompareFiles provides a mock function with given fields: ctx, base, ref +func (_m *MockPullRequestRepo) CompareFiles(ctx context.Context, base string, ref string) ([]repository.VersionedFileChange, error) { + ret := _m.Called(ctx, base, ref) + + if len(ret) == 0 { + panic("no return value specified for CompareFiles") + } + + var r0 []repository.VersionedFileChange + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]repository.VersionedFileChange, error)); ok { + return rf(ctx, base, ref) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) []repository.VersionedFileChange); ok { + r0 = rf(ctx, base, ref) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]repository.VersionedFileChange) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, base, ref) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockPullRequestRepo_CompareFiles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareFiles' +type MockPullRequestRepo_CompareFiles_Call struct { + *mock.Call +} + +// CompareFiles is a helper method to define mock.On call +// - ctx context.Context +// - base string +// - ref string +func (_e *MockPullRequestRepo_Expecter) CompareFiles(ctx interface{}, base interface{}, ref interface{}) *MockPullRequestRepo_CompareFiles_Call { + return &MockPullRequestRepo_CompareFiles_Call{Call: _e.mock.On("CompareFiles", ctx, base, ref)} +} + +func (_c *MockPullRequestRepo_CompareFiles_Call) Run(run func(ctx context.Context, base string, ref string)) *MockPullRequestRepo_CompareFiles_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockPullRequestRepo_CompareFiles_Call) Return(_a0 []repository.VersionedFileChange, _a1 error) *MockPullRequestRepo_CompareFiles_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockPullRequestRepo_CompareFiles_Call) RunAndReturn(run func(context.Context, string, string) ([]repository.VersionedFileChange, error)) *MockPullRequestRepo_CompareFiles_Call { + _c.Call.Return(run) + return _c +} + +// Config provides a mock function with no fields +func (_m *MockPullRequestRepo) Config() *v0alpha1.Repository { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Config") + } + + var r0 *v0alpha1.Repository + if rf, ok := ret.Get(0).(func() *v0alpha1.Repository); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v0alpha1.Repository) + } + } + + return r0 +} + +// MockPullRequestRepo_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config' +type MockPullRequestRepo_Config_Call struct { + *mock.Call +} + +// Config is a helper method to define mock.On call +func (_e *MockPullRequestRepo_Expecter) Config() *MockPullRequestRepo_Config_Call { + return &MockPullRequestRepo_Config_Call{Call: _e.mock.On("Config")} +} + +func (_c *MockPullRequestRepo_Config_Call) Run(run func()) *MockPullRequestRepo_Config_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockPullRequestRepo_Config_Call) Return(_a0 *v0alpha1.Repository) *MockPullRequestRepo_Config_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockPullRequestRepo_Config_Call) RunAndReturn(run func() *v0alpha1.Repository) *MockPullRequestRepo_Config_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function with given fields: ctx, path, ref +func (_m *MockPullRequestRepo) Read(ctx context.Context, path string, ref string) (*repository.FileInfo, error) { + ret := _m.Called(ctx, path, ref) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 *repository.FileInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*repository.FileInfo, error)); ok { + return rf(ctx, path, ref) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *repository.FileInfo); ok { + r0 = rf(ctx, path, ref) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*repository.FileInfo) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, path, ref) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockPullRequestRepo_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockPullRequestRepo_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - ctx context.Context +// - path string +// - ref string +func (_e *MockPullRequestRepo_Expecter) Read(ctx interface{}, path interface{}, ref interface{}) *MockPullRequestRepo_Read_Call { + return &MockPullRequestRepo_Read_Call{Call: _e.mock.On("Read", ctx, path, ref)} +} + +func (_c *MockPullRequestRepo_Read_Call) Run(run func(ctx context.Context, path string, ref string)) *MockPullRequestRepo_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockPullRequestRepo_Read_Call) Return(_a0 *repository.FileInfo, _a1 error) *MockPullRequestRepo_Read_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockPullRequestRepo_Read_Call) RunAndReturn(run func(context.Context, string, string) (*repository.FileInfo, error)) *MockPullRequestRepo_Read_Call { + _c.Call.Return(run) + return _c +} + +// NewMockPullRequestRepo creates a new instance of MockPullRequestRepo. 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 NewMockPullRequestRepo(t interface { + mock.TestingT + Cleanup(func()) +}) *MockPullRequestRepo { + mock := &MockPullRequestRepo{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go b/pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go index 0bb99e8a740..f7aa5bb5039 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package pullrequest diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go b/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go index 0fea17d09b6..c95a37dbb7e 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" ) +//go:generate mockery --name=PullRequestRepo --structname=MockPullRequestRepo --inpackage --filename=mock_pullrequest_repo.go --with-expecter type PullRequestRepo interface { Config() *provisioning.Repository Read(ctx context.Context, path, ref string) (*repository.FileInfo, error) @@ -23,23 +24,25 @@ type PullRequestRepo interface { CommentPullRequest(ctx context.Context, pr int, comment string) error } -type PullRequestWorker struct { - parsers resources.ParserFactory - renderer ScreenshotRenderer - urlProvider func(namespace string) string - commenter *commentBuilder +//go:generate mockery --name=Evaluator --structname=MockEvaluator --inpackage --filename=mock_evaluator.go --with-expecter +type Evaluator interface { + Evaluate(ctx context.Context, repo repository.Reader, opts provisioning.PullRequestJobOptions, changes []repository.VersionedFileChange, progress jobs.JobProgressRecorder) (changeInfo, error) } -func NewPullRequestWorker( - parsers resources.ParserFactory, - renderer ScreenshotRenderer, - urlProvider func(namespace string) string, -) *PullRequestWorker { +//go:generate mockery --name=Commenter --structname=MockCommenter --inpackage --filename=mock_commenter.go --with-expecter +type Commenter interface { + Comment(ctx context.Context, repo PullRequestRepo, pr int, changeInfo changeInfo) error +} + +type PullRequestWorker struct { + evaluator Evaluator + commenter Commenter +} + +func NewPullRequestWorker(evaluator Evaluator, commenter Commenter) *PullRequestWorker { return &PullRequestWorker{ - parsers: parsers, - renderer: renderer, - urlProvider: urlProvider, - commenter: newCommentBuilder(), + evaluator: evaluator, + commenter: commenter, } } @@ -53,15 +56,16 @@ func (c *PullRequestWorker) Process(ctx context.Context, progress jobs.JobProgressRecorder, ) error { cfg := repo.Config().Spec - options := job.Spec.PullRequest - if options == nil { + opts := job.Spec.PullRequest + if opts == nil { return apierrors.NewBadRequest("missing spec.pr") } - if options.Ref == "" { + if opts.Ref == "" { return apierrors.NewBadRequest("missing spec.ref") } + // FIXME: this is leaky because it's supposed to be already a PullRequestRepo if cfg.GitHub == nil { return apierrors.NewBadRequest("expecting github configuration") } @@ -76,51 +80,34 @@ func (c *PullRequestWorker) Process(ctx context.Context, return errors.New("pull request job submitted targeting repository that is not a Reader") } - logger := logging.FromContext(ctx).With("pr", options.PR) + logger := logging.FromContext(ctx).With("pr", opts.PR) logger.Info("process pull request") defer logger.Info("pull request processed") progress.SetMessage(ctx, "listing pull request files") + // FIXME: this is leaky because it's supposed to be already a PullRequestRepo base := cfg.GitHub.Branch - files, err := prRepo.CompareFiles(ctx, base, options.Ref) + files, err := prRepo.CompareFiles(ctx, base, opts.Ref) if err != nil { - return fmt.Errorf("failed to list pull request files: %s", err.Error()) + return fmt.Errorf("failed to list pull request files: %w", err) } files = onlySupportedFiles(files) - if len(files) == 0 { progress.SetFinalMessage(ctx, "no files to process") return nil } - parser, err := c.parsers.GetParser(ctx, reader) + changeInfo, err := c.evaluator.Evaluate(ctx, reader, *opts, files, progress) if err != nil { - return fmt.Errorf("failed to get parser for %s: %w", repo.Config().Name, err) + return fmt.Errorf("calculate changes: %w", err) } - var render ScreenshotRenderer - if cfg.GitHub.GenerateDashboardPreviews { - render = c.renderer - } - - changeInfo, err := processChangedFiles(ctx, changeOptions{ - grafanaBaseURL: c.urlProvider(repo.Config().Namespace), - pullRequest: *options, - changes: files, - parser: parser, - reader: reader, - progress: progress, - render: render, - }) - if err != nil { - return fmt.Errorf("unable to calculate changes: %w", err) - } - - if err := c.commenter.Comment(ctx, prRepo, options.PR, changeInfo); err != nil { + if err := c.commenter.Comment(ctx, prRepo, opts.PR, changeInfo); err != nil { return fmt.Errorf("comment pull request: %w", err) } logger.Info("preview comment added") + return nil } diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 7f8c70d497a..60650cfdd16 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -559,7 +559,9 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH // Pull request worker renderer := pullrequest.NewScreenshotRenderer(b.render, b.unified) - pullRequestWorker := pullrequest.NewPullRequestWorker(b.parsers, renderer, b.urlProvider) + evaluator := pullrequest.NewEvaluator(renderer, b.parsers, b.urlProvider) + commenter := pullrequest.NewCommenter() + pullRequestWorker := pullrequest.NewPullRequestWorker(evaluator, commenter) driver := jobs.NewJobDriver(time.Second*28, time.Second*30, time.Second*30, b.jobs, b, b.jobHistory, exportWorker, syncWorker, migrationWorker, pullRequestWorker) @@ -1114,7 +1116,7 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor return gogit.Clone(ctx, b.clonedir, r, opts, b.secrets) } - return repository.NewGitHub(ctx, r, b.ghFactory, b.secrets, webhookURL, cloneFn) + return repository.NewGitHub(ctx, r, b.ghFactory, b.secrets, webhookURL, cloneFn), nil default: return nil, fmt.Errorf("unknown repository type (%s)", r.Spec.Type) } diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go index 7da5d961409..90b64584666 100644 --- a/pkg/registry/apis/provisioning/repository/github.go +++ b/pkg/registry/apis/provisioning/repository/github.go @@ -12,12 +12,11 @@ import ( "strings" "github.com/google/go-github/v70/github" + "github.com/google/uuid" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" - "github.com/google/uuid" - "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" @@ -57,18 +56,14 @@ func NewGitHub( secrets secrets.Service, webhookURL string, cloneFn CloneFn, -) (*githubRepository, error) { - owner, repo, err := parseOwnerRepo(config.Spec.GitHub.URL) - if err != nil { - return nil, err - } +) *githubRepository { + owner, repo, _ := parseOwnerRepo(config.Spec.GitHub.URL) token := config.Spec.GitHub.Token if token == "" { decrypted, err := secrets.Decrypt(ctx, config.Spec.GitHub.EncryptedToken) - if err != nil { - return nil, err + if err == nil { + token = string(decrypted) } - token = string(decrypted) } return &githubRepository{ config: config, @@ -78,7 +73,7 @@ func NewGitHub( owner: owner, repo: repo, cloneFn: cloneFn, - }, nil + } } func (r *githubRepository) Config() *provisioning.Repository { @@ -138,59 +133,48 @@ func parseOwnerRepo(giturl string) (owner string, repo string, err error) { return parts[1], parts[2], nil } -func fromError(err error, code int) *provisioning.TestResults { - statusErr, ok := err.(apierrors.APIStatus) - if ok { - s := statusErr.Status() - return &provisioning.TestResults{ - Code: int(s.Code), - Success: false, - Errors: []string{s.Message}, - } - } - return &provisioning.TestResults{ - Code: code, - Success: false, - Errors: []string{err.Error()}, - } -} - // Test implements provisioning.Repository. func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { if err := r.gh.IsAuthenticated(ctx); err != nil { - return fromError(err, http.StatusUnauthorized), nil + return &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: field.NewPath("spec", "github", "token").String(), + Detail: err.Error(), + }}}, nil } - owner, repo, err := parseOwnerRepo(r.config.Spec.GitHub.URL) + url := r.config.Spec.GitHub.URL + owner, repo, err := parseOwnerRepo(url) if err != nil { - return fromError(err, http.StatusBadRequest), nil + return fromFieldError(field.Invalid( + field.NewPath("spec", "github", "url"), url, err.Error())), nil } // FIXME: check token permissions ok, err := r.gh.RepoExists(ctx, owner, repo) if err != nil { - return fromError(err, http.StatusBadRequest), nil + return fromFieldError(field.Invalid( + field.NewPath("spec", "github", "url"), url, err.Error())), nil } if !ok { - return &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []string{"repository does not exist"}, - }, nil + return fromFieldError(field.NotFound( + field.NewPath("spec", "github", "url"), url)), nil } - ok, err = r.gh.BranchExists(ctx, r.owner, r.repo, r.config.Spec.GitHub.Branch) + branch := r.config.Spec.GitHub.Branch + ok, err = r.gh.BranchExists(ctx, r.owner, r.repo, branch) if err != nil { - return fromError(err, http.StatusBadRequest), nil + return fromFieldError(field.Invalid( + field.NewPath("spec", "github", "branch"), branch, err.Error())), nil } if !ok { - return &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []string{"branch does not exist"}, - }, nil + return fromFieldError(field.NotFound( + field.NewPath("spec", "github", "branch"), branch)), nil } return &provisioning.TestResults{ diff --git a/pkg/registry/apis/provisioning/repository/local.go b/pkg/registry/apis/provisioning/repository/local.go index d28663e4680..7c5a237a94c 100644 --- a/pkg/registry/apis/provisioning/repository/local.go +++ b/pkg/registry/apis/provisioning/repository/local.go @@ -146,36 +146,19 @@ func (r *localRepository) Validate() (fields field.ErrorList) { // Test implements provisioning.Repository. // NOTE: Validate has been called (and passed) before this function should be called func (r *localRepository) Test(ctx context.Context) (*provisioning.TestResults, error) { + path := field.NewPath("spec", "localhost", "path") if r.config.Spec.Local.Path == "" { - return &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []string{ - "no path is configured", - }, - }, nil + return fromFieldError(field.Required(path, "no path is configured")), nil } _, err := r.resolver.LocalPath(r.config.Spec.Local.Path) if err != nil { - return &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []string{ - err.Error(), - }, - }, nil + return fromFieldError(field.Invalid(path, r.config.Spec.Local.Path, err.Error())), nil } _, err = os.Stat(r.path) if errors.Is(err, os.ErrNotExist) { - return &provisioning.TestResults{ - Code: http.StatusBadRequest, - Success: false, - Errors: []string{ - fmt.Sprintf("directory not found: %s", r.config.Spec.Local.Path), - }, - }, nil + return fromFieldError(field.NotFound(path, r.config.Spec.Local.Path)), nil } return &provisioning.TestResults{ diff --git a/pkg/registry/apis/provisioning/repository/test.go b/pkg/registry/apis/provisioning/repository/test.go index 8ba064ea08c..76f5b4d4f67 100644 --- a/pkg/registry/apis/provisioning/repository/test.go +++ b/pkg/registry/apis/provisioning/repository/test.go @@ -6,6 +6,7 @@ import ( "net/http" "slices" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" @@ -26,10 +27,14 @@ func TestRepository(ctx context.Context, repo Repository) (*provisioning.TestRes rsp := &provisioning.TestResults{ Code: http.StatusUnprocessableEntity, // Invalid Success: false, - Errors: make([]string, len(errors)), + Errors: make([]provisioning.ErrorDetails, len(errors)), } - for i, v := range errors { - rsp.Errors[i] = v.Error() + for i, err := range errors { + rsp.Errors[i] = provisioning.ErrorDetails{ + Type: metav1.CauseType(err.Type), + Field: err.Field, + Detail: err.Detail, + } } return rsp, nil } @@ -85,3 +90,15 @@ func ValidateRepository(repo Repository) field.ErrorList { return list } + +func fromFieldError(err *field.Error) *provisioning.TestResults { + return &provisioning.TestResults{ + Code: http.StatusBadRequest, + Success: false, + Errors: []provisioning.ErrorDetails{{ + Type: metav1.CauseType(err.Type), + Field: err.Field, + Detail: err.Detail, + }}, + } +} diff --git a/pkg/registry/apis/provisioning/repository/test_test.go b/pkg/registry/apis/provisioning/repository/test_test.go index 4e7b726ea56..1dd09596617 100644 --- a/pkg/registry/apis/provisioning/repository/test_test.go +++ b/pkg/registry/apis/provisioning/repository/test_test.go @@ -6,11 +6,12 @@ import ( "net/http" "testing" - provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" + + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) func TestValidateRepository(t *testing.T) { @@ -192,7 +193,7 @@ func TestTestRepository(t *testing.T) { name string repository *MockRepository expectedCode int - expectedErrs []string + expectedErrs []provisioning.ErrorDetails expectedError error }{ { @@ -208,7 +209,11 @@ func TestTestRepository(t *testing.T) { return m }(), expectedCode: http.StatusUnprocessableEntity, - expectedErrs: []string{"spec.title: Required value: a repository title must be given"}, + expectedErrs: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueRequired, + Field: "spec.title", + Detail: "a repository title must be given", + }}, }, { name: "test passes", @@ -257,12 +262,18 @@ func TestTestRepository(t *testing.T) { m.On("Test", mock.Anything).Return(&provisioning.TestResults{ Code: http.StatusBadRequest, Success: false, - Errors: []string{"test failed"}, + Errors: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "spec.property", + }}, }, nil) return m }(), expectedCode: http.StatusBadRequest, - expectedErrs: []string{"test failed"}, + expectedErrs: []provisioning.ErrorDetails{{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "spec.property", + }}, }, } diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index b8313aa99d7..bfd62fe0db3 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -7,8 +7,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/google/uuid" - authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" @@ -40,22 +38,26 @@ func (r *DualReadWriter) Read(ctx context.Context, path string, ref string) (*Pa info, err := r.repo.Read(ctx, path, ref) if err != nil { - return nil, fmt.Errorf("read file: %w", err) + _, ok := utils.ExtractApiErrorStatus(err) + if ok { + return nil, err + } + return nil, fmt.Errorf("Read file failed: %w", err) } parsed, err := r.parser.Parse(ctx, info) if err != nil { - return nil, fmt.Errorf("parse file: %w", err) - } - - // Authorize the parsed resource - if err = r.authorize(ctx, parsed, utils.VerbGet); err != nil { - return nil, err + return nil, apierrors.NewBadRequest(fmt.Sprintf("Parse file failed: %v", err)) } // Fail as we use the dry run for this response and it's not about updating the resource if err := parsed.DryRun(ctx); err != nil { - return nil, fmt.Errorf("run dry run: %w", err) + return nil, apierrors.NewBadRequest(fmt.Sprintf("Dry run failed: %v", err)) + } + + // Authorize based on the existing resource + if err = r.authorize(ctx, parsed, utils.VerbGet); err != nil { + return nil, err } return parsed, nil @@ -195,21 +197,6 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, path s return nil, err } - // Verify that we can create (or update) the referenced resource - verb := utils.VerbUpdate - if create { - verb = utils.VerbCreate - } - if err = r.authorize(ctx, parsed, verb); err != nil { - return nil, err - } - - // Always use the provisioning identity when writing - ctx, _, err = identity.WithProvisioningIdentity(ctx, parsed.Obj.GetNamespace()) - if err != nil { - return nil, fmt.Errorf("unable to use provisioning identity %w", err) - } - // Make sure the value is valid if err := parsed.DryRun(ctx); err != nil { logger := logging.FromContext(ctx).With("path", path, "name", parsed.Obj.GetName(), "ref", ref) @@ -224,11 +211,26 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, path s return nil, fmt.Errorf("errors while parsing file [%v]", parsed.Errors) } + // Verify that we can create (or update) the referenced resource + verb := utils.VerbUpdate + if parsed.Action == provisioning.ResourceActionCreate { + verb = utils.VerbCreate + } + if err = r.authorize(ctx, parsed, verb); err != nil { + return nil, err + } + data, err = parsed.ToSaveBytes() if err != nil { return nil, err } + // Always use the provisioning identity when writing + ctx, _, err = identity.WithProvisioningIdentity(ctx, parsed.Obj.GetNamespace()) + if err != nil { + return nil, fmt.Errorf("unable to use provisioning identity %w", err) + } + // Create or update if create { err = r.repo.Create(ctx, path, ref, data, message) @@ -255,49 +257,47 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, path s } func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, verb string) error { - auth, ok := authlib.AuthInfoFrom(ctx) - if !ok { - return fmt.Errorf("missing auth info in context") - } - rsp, err := r.access.Check(ctx, auth, authlib.CheckRequest{ - Group: parsed.GVR.Group, - Resource: parsed.GVR.Resource, - Namespace: parsed.Obj.GetNamespace(), - Name: parsed.Obj.GetName(), - Folder: parsed.Meta.GetFolder(), - Verb: verb, - }) + id, err := identity.GetRequester(ctx) if err != nil { - return err + return apierrors.NewUnauthorized(err.Error()) } - if !rsp.Allowed { - return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), - fmt.Errorf("no access to see embedded file")) + + // Use configured permissions for get+delete + if parsed.Existing != nil && (verb == utils.VerbGet || verb == utils.VerbDelete) { + rsp, err := r.access.Check(ctx, id, authlib.CheckRequest{ + Group: parsed.GVR.Group, + Resource: parsed.GVR.Resource, + Namespace: parsed.Existing.GetNamespace(), + Name: parsed.Existing.GetName(), + Folder: parsed.Meta.GetFolder(), + Verb: utils.VerbGet, + }) + if err != nil || !rsp.Allowed { + return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), + fmt.Errorf("no access to read the embedded file")) + } } - return nil + + // Simple role based access for now + if id.GetOrgRole().Includes(identity.RoleEditor) { + return nil + } + + return apierrors.NewForbidden(parsed.GVR.GroupResource(), parsed.Obj.GetName(), + fmt.Errorf("must be admin or editor to access files from provisioning")) } func (r *DualReadWriter) authorizeCreateFolder(ctx context.Context, _ string) error { - auth, ok := authlib.AuthInfoFrom(ctx) - if !ok { - return fmt.Errorf("missing auth info in context") - } - rsp, err := r.access.Check(ctx, auth, authlib.CheckRequest{ - Group: FolderResource.Group, - Resource: FolderResource.Resource, - Namespace: r.repo.Config().GetNamespace(), - Verb: utils.VerbCreate, - - // TODO: Currently this checks if you can create a new folder in root - // Ideally we should check the path and use the explicit parent and new id - Name: "f" + uuid.NewString(), - }) + id, err := identity.GetRequester(ctx) if err != nil { - return err + return apierrors.NewUnauthorized(err.Error()) } - if !rsp.Allowed { - return apierrors.NewForbidden(FolderResource.GroupResource(), "", - fmt.Errorf("unable to create folder resource")) + + // Simple role based access for now + if id.GetOrgRole().Includes(identity.RoleEditor) { + return nil } - return nil + + return apierrors.NewForbidden(FolderResource.GroupResource(), "", + fmt.Errorf("must be admin or editor to access folders with provisioning")) } diff --git a/pkg/registry/apis/provisioning/resources/fileformat.go b/pkg/registry/apis/provisioning/resources/fileformat.go index dfff81efa62..0ed0b287199 100644 --- a/pkg/registry/apis/provisioning/resources/fileformat.go +++ b/pkg/registry/apis/provisioning/resources/fileformat.go @@ -19,8 +19,11 @@ import ( ) var ( - ErrUnableToReadResourceBytes = errors.New("unable to read bytes as a resource") - ErrClassicResourceIsAlreadyK8sForm = errors.New("classic resource is already structured with apiVersion and kind") + ErrUnableToReadResourceBytes = errors.New("unable to read bytes as a resource") + ErrUnableToReadPanelsMissing = errors.New("panels property is required") + ErrUnableToReadSchemaVersionMissing = errors.New("schemaVersion property is required") + ErrUnableToReadTagsMissing = errors.New("tags property is required") + ErrClassicResourceIsAlreadyK8sForm = errors.New("classic resource is already structured with apiVersion and kind") ) // This reads a "classic" file format and will convert it to an unstructured k8s resource @@ -37,7 +40,7 @@ func ReadClassicResource(ctx context.Context, info *repository.FileInfo) (*unstr return nil, nil, "", err } } else { - return nil, nil, "", fmt.Errorf("classic resource must be JSON") + return nil, nil, "", fmt.Errorf("unable to read file") } // regular version headers exist @@ -64,7 +67,7 @@ func ReadClassicResource(ctx context.Context, info *repository.FileInfo) (*unstr value["tags"] != nil { gvk := &schema.GroupVersionKind{ Group: dashboard.GROUP, - Version: dashboard.VERSION, // v1 + Version: "v0alpha1", // no schema Kind: "Dashboard"} return &unstructured.Unstructured{ Object: map[string]interface{}{ diff --git a/pkg/registry/apis/provisioning/resources/parser.go b/pkg/registry/apis/provisioning/resources/parser.go index cdca95ea405..0601a9f1e17 100644 --- a/pkg/registry/apis/provisioning/resources/parser.go +++ b/pkg/registry/apis/provisioning/resources/parser.go @@ -138,7 +138,7 @@ func (r *parser) Parse(ctx context.Context, info *repository.FileInfo) (parsed * logger.Debug("failed to find GVK of the input data, trying fallback loader", "error", err) parsed.Obj, gvk, parsed.Classic, err = ReadClassicResource(ctx, info) if err != nil || gvk == nil { - return nil, err + return nil, apierrors.NewBadRequest("unable to read file as a resource") } } @@ -227,18 +227,25 @@ func (f *ParsedResource) DryRun(ctx context.Context) error { return err } + fieldValidation := "Strict" + if f.GVR == DashboardResource { + fieldValidation = "Ignore" // FIXME: temporary while we improve validation + } + // FIXME: shouldn't we check for the specific error? // Dry run CREATE or UPDATE f.Existing, _ = f.Client.Get(ctx, f.Obj.GetName(), metav1.GetOptions{}) if f.Existing == nil { f.Action = provisioning.ResourceActionCreate f.DryRunResponse, err = f.Client.Create(ctx, f.Obj, metav1.CreateOptions{ - DryRun: []string{"All"}, + DryRun: []string{"All"}, + FieldValidation: fieldValidation, }) } else { f.Action = provisioning.ResourceActionUpdate f.DryRunResponse, err = f.Client.Update(ctx, f.Obj, metav1.UpdateOptions{ - DryRun: []string{"All"}, + DryRun: []string{"All"}, + FieldValidation: fieldValidation, }) } return err @@ -262,13 +269,22 @@ func (f *ParsedResource) Run(ctx context.Context) error { f.Existing, _ = f.Client.Get(ctx, f.Obj.GetName(), metav1.GetOptions{}) } + fieldValidation := "Strict" + if f.GVR == DashboardResource { + fieldValidation = "Ignore" // FIXME: temporary while we improve validation + } + // Run update or create if f.Existing == nil { f.Action = provisioning.ResourceActionCreate - f.Upsert, err = f.Client.Create(ctx, f.Obj, metav1.CreateOptions{}) + f.Upsert, err = f.Client.Create(ctx, f.Obj, metav1.CreateOptions{ + FieldValidation: fieldValidation, + }) } else { f.Action = provisioning.ResourceActionUpdate - f.Upsert, err = f.Client.Update(ctx, f.Obj, metav1.UpdateOptions{}) + f.Upsert, err = f.Client.Update(ctx, f.Obj, metav1.UpdateOptions{ + FieldValidation: fieldValidation, + }) } return err diff --git a/pkg/registry/apis/provisioning/resources/parser_test.go b/pkg/registry/apis/provisioning/resources/parser_test.go index 1a02854ae2b..bf545f90b07 100644 --- a/pkg/registry/apis/provisioning/resources/parser_test.go +++ b/pkg/registry/apis/provisioning/resources/parser_test.go @@ -34,7 +34,7 @@ func TestParser(t *testing.T) { Data: []byte("hello"), // not a real resource }) require.Error(t, err) - require.Equal(t, "classic resource must be JSON", err.Error()) + require.Equal(t, "unable to read file as a resource", err.Error()) }) t.Run("dashboard parsing (with and without name)", func(t *testing.T) { diff --git a/pkg/registry/apis/provisioning/resources/resources.go b/pkg/registry/apis/provisioning/resources/resources.go index 26d79bf4485..7836581ddd1 100644 --- a/pkg/registry/apis/provisioning/resources/resources.go +++ b/pkg/registry/apis/provisioning/resources/resources.go @@ -177,10 +177,20 @@ func (r *ResourcesManager) WriteResourceFromFile(ctx context.Context, path strin parsed.Meta.SetUID("") parsed.Meta.SetResourceVersion("") + // TODO: use parsed.Run() (but that has an extra GET now!!) + fieldValidation := "Strict" + if parsed.GVR == DashboardResource { + fieldValidation = "Ignore" // FIXME: temporary while we improve validation + } + // Update or Create resource - parsed.Upsert, err = parsed.Client.Update(ctx, parsed.Obj, metav1.UpdateOptions{}) + parsed.Upsert, err = parsed.Client.Update(ctx, parsed.Obj, metav1.UpdateOptions{ + FieldValidation: fieldValidation, + }) if apierrors.IsNotFound(err) { - parsed.Upsert, err = parsed.Client.Create(ctx, parsed.Obj, metav1.CreateOptions{}) + parsed.Upsert, err = parsed.Client.Create(ctx, parsed.Obj, metav1.CreateOptions{ + FieldValidation: fieldValidation, + }) } return parsed.Obj.GetName(), parsed.GVK, err diff --git a/pkg/registry/apis/provisioning/test.go b/pkg/registry/apis/provisioning/test.go index 85c8b3a2bf8..8197b00a96d 100644 --- a/pkg/registry/apis/provisioning/test.go +++ b/pkg/registry/apis/provisioning/test.go @@ -99,9 +99,9 @@ func (t *RepositoryTester) UpdateHealthStatus(ctx context.Context, cfg *provisio if res == nil { res = &provisioning.TestResults{ Success: false, - Errors: []string{ - "missing health status", - }, + Errors: []provisioning.ErrorDetails{{ + Detail: "missing health status", + }}, } } @@ -109,7 +109,11 @@ func (t *RepositoryTester) UpdateHealthStatus(ctx context.Context, cfg *provisio repo.Status.Health = provisioning.HealthStatus{ Healthy: res.Success, Checked: time.Now().UnixMilli(), - Message: res.Errors, + } + for _, err := range res.Errors { + if err.Detail != "" { + repo.Status.Health.Message = append(repo.Status.Health.Message, err.Detail) + } } _, err := t.client.Repositories(repo.GetNamespace()). diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 9c1e3ea66b4..d4d9ca9a255 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -24,8 +24,8 @@ import ( type K8sHandler interface { GetNamespace(orgID int64) string Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) - Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) - Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) + Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.CreateOptions) (*unstructured.Unstructured, error) + Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.UpdateOptions) (*unstructured.Unstructured, error) Delete(ctx context.Context, name string, orgID int64, options v1.DeleteOptions) error DeleteCollection(ctx context.Context, orgID int64) error List(ctx context.Context, orgID int64, options v1.ListOptions) (*unstructured.UnstructuredList, error) @@ -71,22 +71,22 @@ func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, options return client.Get(ctx, name, options, subresource...) } -func (h *k8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) { +func (h *k8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.CreateOptions) (*unstructured.Unstructured, error) { client, err := h.getClient(ctx, orgID) if err != nil { return nil, err } - return client.Create(ctx, obj, v1.CreateOptions{}) + return client.Create(ctx, obj, opts) } -func (h *k8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) { +func (h *k8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.UpdateOptions) (*unstructured.Unstructured, error) { client, err := h.getClient(ctx, orgID) if err != nil { return nil, err } - return client.Update(ctx, obj, v1.UpdateOptions{}) + return client.Update(ctx, obj, opts) } func (h *k8sHandler) Delete(ctx context.Context, name string, orgID int64, options v1.DeleteOptions) error { diff --git a/pkg/services/apiserver/client/client_mock.go b/pkg/services/apiserver/client/client_mock.go index f17c89daf67..2cc34b89d95 100644 --- a/pkg/services/apiserver/client/client_mock.go +++ b/pkg/services/apiserver/client/client_mock.go @@ -32,16 +32,16 @@ func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, opti return args.Get(0).(*unstructured.Unstructured), args.Error(1) } -func (m *MockK8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) { - args := m.Called(ctx, obj, orgID) +func (m *MockK8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.CreateOptions) (*unstructured.Unstructured, error) { + args := m.Called(ctx, obj, orgID, opts) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*unstructured.Unstructured), args.Error(1) } -func (m *MockK8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) { - args := m.Called(ctx, obj, orgID) +func (m *MockK8sHandler) Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64, opts v1.UpdateOptions) (*unstructured.Unstructured, error) { + args := m.Called(ctx, obj, orgID, opts) if args.Get(0) == nil { return nil, args.Error(1) } diff --git a/pkg/services/authz/rbac.go b/pkg/services/authz/rbac.go index 3822596d6ba..e15603b7461 100644 --- a/pkg/services/authz/rbac.go +++ b/pkg/services/authz/rbac.go @@ -8,11 +8,6 @@ import ( "time" "github.com/fullstorydev/grpchan/inprocgrpc" - authnlib "github.com/grafana/authlib/authn" - authzlib "github.com/grafana/authlib/authz" - authzv1 "github.com/grafana/authlib/authz/proto/v1" - "github.com/grafana/authlib/cache" - authlib "github.com/grafana/authlib/types" grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/trace" @@ -21,6 +16,11 @@ import ( "google.golang.org/grpc/credentials/insecure" "k8s.io/client-go/rest" + authnlib "github.com/grafana/authlib/authn" + authzlib "github.com/grafana/authlib/authz" + authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/authlib/cache" + authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -60,6 +60,13 @@ func ProvideAuthZClient( return nil, errors.New("authZGRPCServer feature toggle is required for cloud and grpc mode") } + // Provisioning uses mode 4 (read+write only to unified storage) + // For G12 launch, we can disable caching for this and find a more scalable solution soon + // most likely this would involve passing the RV (timestamp!) in each check method + if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { + authCfg.cacheTTL = 0 + } + switch authCfg.mode { case clientModeCloud: rbacClient, err := newRemoteRBACClient(authCfg, tracer) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index a29c77f7a37..d2cf210d919 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" ) @@ -434,7 +435,13 @@ func (ss *sqlStore) getSnapshotResources(ctx context.Context, snapshotUid string if errorsOnly { sess.Where("status = ?", cloudmigration.ItemStatusError) } - return sess.OrderBy(fmt.Sprintf("%s %s", col, dir)).Find(&resources, &cloudmigration.CloudMigrationResource{ + // TODO: It would be better if the query builder supported a case-insensitive flag for the .OrderBy() method + orderByClause := fmt.Sprintf("lower(%s) %s", col, dir) + if ss.db.GetDBType() == migrator.Postgres || // Postgres does not support lower() in ORDER BY -- sorts by case-insensitive by default + params.SortColumn == cloudmigration.SortColumnID { // Don't apply a string sort to a numeric column + orderByClause = fmt.Sprintf("%s %s", col, dir) + } + return sess.OrderBy(orderByClause).Find(&resources, &cloudmigration.CloudMigrationResource{ SnapshotUID: snapshotUid, }) }) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 530c489d19b..b64dd11dc85 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -359,6 +359,77 @@ func Test_SnapshotResources(t *testing.T) { }) } +func Test_SnapshotResourceCaseInsensitiveSorting(t *testing.T) { + t.Parallel() + + _, s := setUpTest(t) + ctx := context.Background() + + // Create test data with mixed case names + resources := []cloudmigration.CloudMigrationResource{ + {UID: "1", SnapshotUID: "abc123", Name: "B", Type: cloudmigration.DashboardDataType, Status: cloudmigration.ItemStatusOK}, + {UID: "2", SnapshotUID: "abc123", Name: "aa", Type: cloudmigration.AlertRuleType, Status: cloudmigration.ItemStatusOK}, + {UID: "3", SnapshotUID: "abc123", Name: "ba", Type: cloudmigration.DashboardDataType, Status: cloudmigration.ItemStatusOK}, + {UID: "4", SnapshotUID: "abc123", Name: "A", Type: cloudmigration.AlertRuleType, Status: cloudmigration.ItemStatusOK}, + } + + err := s.db.WithDbSession(ctx, func(sess *db.Session) error { + _, err := sess.Insert(resources) + return err + }) + require.NoError(t, err) + + // Test ascending sort + results, err := s.getSnapshotResources(ctx, "abc123", cloudmigration.SnapshotResultQueryParams{ + ResultPage: 1, + ResultLimit: 100, + SortColumn: cloudmigration.SortColumnName, + SortOrder: cloudmigration.SortOrderAsc, + }) + require.NoError(t, err) + assert.True(t, testNameComesBefore(t, results, "A", "aa")) + assert.True(t, testNameComesBefore(t, results, "B", "ba")) + assert.True(t, testNameComesBefore(t, results, "A", "B")) + assert.True(t, testNameComesBefore(t, results, "aa", "B")) + assert.True(t, testNameComesBefore(t, results, "aa", "ba")) + assert.True(t, testNameComesBefore(t, results, "A", "ba")) + assert.True(t, testNameComesBefore(t, results, "A", "B")) + + // Test descending sort + results, err = s.getSnapshotResources(ctx, "abc123", cloudmigration.SnapshotResultQueryParams{ + ResultPage: 1, + ResultLimit: 100, + SortColumn: cloudmigration.SortColumnName, + SortOrder: cloudmigration.SortOrderDesc, + }) + require.NoError(t, err) + assert.True(t, testNameComesBefore(t, results, "ba", "B")) + assert.True(t, testNameComesBefore(t, results, "aa", "A")) + assert.True(t, testNameComesBefore(t, results, "ba", "B")) + assert.True(t, testNameComesBefore(t, results, "aa", "A")) + assert.True(t, testNameComesBefore(t, results, "ba", "B")) + assert.True(t, testNameComesBefore(t, results, "aa", "A")) +} + +func testNameComesBefore(t *testing.T, input []cloudmigration.CloudMigrationResource, first string, second string) bool { + t.Helper() + + foundFirst, foundSecond := false, false + for _, r := range input { + if r.Name == second { + foundSecond = true + continue + } + if r.Name == first { + if foundSecond { + return false + } + foundFirst = true + } + } + return foundFirst && foundSecond +} + func TestGetSnapshotList(t *testing.T) { t.Parallel() diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index eed74193348..77251a18e64 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1833,10 +1833,14 @@ func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.C meta.SetManagerProperties(m) meta.SetSourceProperties(s) - out, err := dr.k8sclient.Update(ctx, obj, cmd.OrgID) + out, err := dr.k8sclient.Update(ctx, obj, cmd.OrgID, v1.UpdateOptions{ + FieldValidation: v1.FieldValidationIgnore, + }) if err != nil && apierrors.IsNotFound(err) { // Create if it doesn't already exist. - out, err = dr.k8sclient.Create(ctx, obj, cmd.OrgID) + out, err = dr.k8sclient.Create(ctx, obj, cmd.OrgID, v1.CreateOptions{ + FieldValidation: v1.FieldValidationIgnore, + }) if err != nil { return nil, err } @@ -1854,10 +1858,14 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd } dashboard.SetPluginIDMeta(obj, cmd.PluginID) - out, err := dr.k8sclient.Update(ctx, obj, orgID) + out, err := dr.k8sclient.Update(ctx, obj, orgID, v1.UpdateOptions{ + FieldValidation: v1.FieldValidationIgnore, + }) if err != nil && apierrors.IsNotFound(err) { // Create if it doesn't already exist. - out, err = dr.k8sclient.Create(ctx, obj, orgID) + out, err = dr.k8sclient.Create(ctx, obj, orgID, v1.CreateOptions{ + FieldValidation: v1.FieldValidationIgnore, + }) if err != nil { return nil, err } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 313edeba510..0ff3050f3ee 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -1133,7 +1133,9 @@ func TestUnprovisionDashboard(t *testing.T) { }, }} // should update it to be without annotations - k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything).Return(dashWithoutAnnotations, nil) + k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, metav1.UpdateOptions{ + FieldValidation: metav1.FieldValidationIgnore, + }).Return(dashWithoutAnnotations, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{ @@ -1357,7 +1359,9 @@ func TestSaveProvisionedDashboard(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil) k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) - k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ + FieldValidation: metav1.FieldValidationIgnore, + }).Return(&dashboardUnstructured, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") dashboard, err := service.SaveProvisionedDashboard(ctx, query, &dashboards.DashboardProvisioning{}) @@ -1419,7 +1423,9 @@ func TestSaveDashboard(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") - k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ + FieldValidation: metav1.FieldValidationIgnore, + }).Return(&dashboardUnstructured, nil) dashboard, err := service.SaveDashboard(ctx, query, false) require.NoError(t, err) @@ -1430,7 +1436,9 @@ func TestSaveDashboard(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") - k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ + FieldValidation: metav1.FieldValidationIgnore, + }).Return(&dashboardUnstructured, nil) dashboard, err := service.SaveDashboard(ctx, query, false) require.NoError(t, err) @@ -1440,7 +1448,9 @@ func TestSaveDashboard(t *testing.T) { t.Run("Should return an error if uid is invalid", func(t *testing.T) { ctx, k8sCliMock := setupK8sDashboardTests(service) k8sCliMock.On("GetNamespace", mock.Anything).Return("default") - k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil) + k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, metav1.UpdateOptions{ + FieldValidation: metav1.FieldValidationIgnore, + }).Return(&dashboardUnstructured, nil) query.Dashboard.UID = "invalid/uid" _, err := service.SaveDashboard(ctx, query, false) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 568a33f60ac..c96090f0e2d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -206,14 +206,6 @@ var ( FrontendOnly: false, Owner: grafanaPartnerPluginsSquad, }, - { - Name: "prometheusRunQueriesInParallel", - Description: "Enables running Prometheus queries in parallel", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: false, - Owner: grafanaOSSBigTent, - Expression: "true", // enabled by default - }, { Name: "lokiLogsDataplane", Description: "Changes logs responses from Loki to be compliant with the dataplane specification.", @@ -489,13 +481,6 @@ var ( Owner: grafanaPluginsPlatformSquad, Expression: "true", // enabled by default }, - { - Name: "pluginsAPIMetrics", - Description: "Sends metrics of public grafana packages usage by plugins", - FrontendOnly: true, - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - }, { Name: "externalServiceAccounts", Description: "Automatic service account and token setup for plugins", @@ -564,6 +549,25 @@ var ( { Name: "kubernetesClientDashboardsFolders", Description: "Route the folder and dashboard service requests to k8s", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAppPlatformSquad, + Expression: "true", // enabled by default + }, + { + Name: "dashboardDisableSchemaValidationV1", + Description: "Disable schema validation for dashboards/v1", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + }, + { + Name: "dashboardDisableSchemaValidationV2", + Description: "Disable schema validation for dashboards/v2", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + }, + { + Name: "dashboardSchemaValidationLogging", + Description: "Log schema validation errors so they can be analyzed later", Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, }, @@ -750,14 +754,6 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, - { - Name: "exploreMetrics", - Description: "Enables the new Grafana Metrics Drilldown core app", - Stage: FeatureStageGeneralAvailability, - Expression: "true", // enabled by default - FrontendOnly: true, - Owner: grafanaObservabilityMetricsSquad, - }, { Name: "alertingSimplifiedRouting", Description: "Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule", @@ -1545,15 +1541,6 @@ var ( Stage: FeatureStageExperimental, Owner: awsDatasourcesSquad, }, - { - Name: "exploreMetricsUseExternalAppPlugin", - Description: "Use the externalized Grafana Metrics Drilldown (formerly known as Explore Metrics) app plugin", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaObservabilityMetricsSquad, - Expression: "true", - FrontendOnly: false, - RequiresRestart: true, - }, { Name: "datasourceConnectionsTab", Description: "Shows defined connections for a data source in the plugins detail page", @@ -1602,12 +1589,21 @@ var ( HideFromDocs: true, }, { - Name: "useScopesNavigationEndpoint", - Description: "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendPlatformSquad, - FrontendOnly: true, - HideFromDocs: true, + Name: "useScopesNavigationEndpoint", + Description: "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + FrontendOnly: true, + HideFromDocs: true, + HideFromAdminPage: true, + }, + { + Name: "scopeSearchAllLevels", + Description: "Enable scope search to include all levels of the scope node tree", + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + HideFromDocs: true, + HideFromAdminPage: true, }, { Name: "alertingRuleVersionHistoryRestore", diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index 4e787933b8b..b7f4ab02da4 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -8,10 +8,10 @@ fullRangeLogsVolume,2022-01-26T17:44:20Z,2022-02-15T08:05:03Z,5d66194ec5d8a7c174 httpclientprovider_azure_auth,2022-01-26T17:44:20Z,2022-05-30T15:43:32Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley live-config,2022-01-26T17:44:20Z,2023-02-03T21:21:48Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley live-pipeline,2022-01-26T17:44:20Z,2023-03-22T18:09:44Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley -live-service-web-worker,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley +live-service-web-worker,2022-01-26T17:44:20Z,2025-04-07T14:47:35Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley newNavigation,2022-01-26T17:44:20Z,2022-06-16T09:48:38Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley prometheus_azure_auth,2022-01-26T17:44:20Z,2022-08-11T14:12:57Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley -queryOverLive,2022-01-26T17:44:20Z,,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley +queryOverLive,2022-01-26T17:44:20Z,2025-04-07T14:47:35Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley service-accounts,2022-01-26T17:44:20Z,2022-04-21T09:41:37Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley showFeatureFlagsInUI,2022-01-26T17:44:20Z,2023-02-09T00:01:34Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley tempoBackendSearch,2022-01-26T17:44:20Z,2022-06-01T17:32:10Z,5d66194ec5d8a7c174a075b6023dcbf58d17b861,Ryan McKinley @@ -86,7 +86,7 @@ newPanelChromeUI,2022-10-10T15:03:16Z,2023-09-12T08:54:41Z,129a5a29e78853bb1427c mysqlAnsiQuotes,2022-10-12T11:43:35Z,,80ede174dd6e343722cc708ebc4c5f21fb9e90af,ying-jeanne showDashboardValidationWarnings,2022-10-14T13:51:05Z,,2e16d5499e1cf29e67db5031feda09407f69550c,Josh Hunt interFont,2022-10-15T14:22:33Z,2022-12-01T11:59:37Z,9f5e691994c9db15f5984b196ab55fd7213b7f72,Torkel Ödegaard -accessControlOnCall,2022-10-19T16:10:09Z,,717bd4a6c051d1447c9e35385a4ba176dd391c65,Gabriel MABILLE +accessControlOnCall,2022-10-19T16:10:09Z,2025-02-25T12:44:40Z,717bd4a6c051d1447c9e35385a4ba176dd391c65,Gabriel MABILLE newDBLibrary,2022-10-26T01:20:41Z,2023-11-14T14:51:35Z,a3acfb1a48126119fd4913efbb572de162264e92,Ryan McKinley nestedFolders,2022-10-26T14:15:14Z,,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert datasourceLogger,2022-11-02T13:51:51Z,2023-02-07T11:49:16Z,06705a49e236dc3ab3b35db51b7bad8625e4866f,Carl Bergquist @@ -106,7 +106,7 @@ alertingBacktesting,2022-12-14T14:44:14Z,,ad09feed837737c188e37fc6adfead7b8a4571 disablePrometheusExemplarSampling,2022-12-19T15:00:15Z,2024-01-30T16:00:04Z,f67b8fe0dc2a2d860e71887a4d2e6e25da80bd8f,Ludovic Viaud supportBundles,2022-12-20T10:13:37Z,2023-02-10T09:12:04Z,2c7410c87d81b8a644e738fee2bb6f34355111c6,Jo publicDashboardsEmailSharing,2023-01-03T19:45:15Z,,f0ee3ac80ae2af695c56e709af435c155a2fb33d,owensmallwood -alertingNoNormalState,2023-01-13T23:29:29Z,,9d57b1c72eb817c5fe1b43ad7bc9706a264b2da0,Yuri Tseretyan +alertingNoNormalState,2023-01-13T23:29:29Z,2025-02-03T15:32:50Z,9d57b1c72eb817c5fe1b43ad7bc9706a264b2da0,Yuri Tseretyan azureMultipleResourcePicker,2023-01-20T15:29:23Z,2023-01-30T16:19:03Z,b1efd911c1f7bfabe1b8231b059470c72dce4712,Andres Martinez Gotor editPanelCSVDragAndDrop,2023-01-24T09:43:44Z,,4167214e3535f41c85004c6aca86fcb9ae8956dd,Zoltán Bedi topNavCommandPalette,2023-01-24T12:41:09Z,2023-02-24T12:14:53Z,88347caf5fcc99c21c8a07367b9e846947367636,Josh Hunt @@ -123,15 +123,15 @@ prometheusMetricEncyclopedia,2023-03-07T18:41:05Z,2024-12-30T21:16:04Z,9b6e53154 timeSeriesTable,2023-03-10T12:41:06Z,2023-10-13T08:00:42Z,548a5054ad8fa5c199bd01e20a02fbbe558d52fe,Domas lokiQuerySplittingConfig,2023-03-20T15:51:36Z,,68551ac9ca5b92621e6ab895c759822797bbf418,Sven Grossmann onlyExternalOrgRoleSync,2023-03-22T17:41:59Z,2023-07-25T09:51:47Z,3cd952b8bad8d23daee10fcc303a6d19c8b6d0a0,Eric Leijonmarck -autoMigrateOldPanels,2023-03-23T04:02:36Z,,baf5a1d1411ee7e5f297aba099e735ebeee3ab83,Ryan McKinley +autoMigrateOldPanels,2023-03-23T04:02:36Z,2025-04-04T09:31:35Z,baf5a1d1411ee7e5f297aba099e735ebeee3ab83,Ryan McKinley clientTokenRotation,2023-03-23T13:39:04Z,2024-02-16T14:03:37Z,382b24742ab8b124af072533f74027a1a103b037,Karl Persson -disableAngular,2023-03-23T15:43:45Z,,58704390269c8eeedd32c571245d5ef08f0ffb82,Ryan McKinley +disableAngular,2023-03-23T15:43:45Z,2025-04-04T09:31:35Z,58704390269c8eeedd32c571245d5ef08f0ffb82,Ryan McKinley disableElasticsearchBackendExploreQuery,2023-03-27T13:52:27Z,2023-04-12T12:20:43Z,f3da91f53fa32a8e870124e9514a010b97359529,Ivana Huckova emptyDashboardPage,2023-03-28T09:42:23Z,2024-01-25T14:04:29Z,221c5efedc593fb42d87c849b3d0827e325fc6cd,Polina Boneva prometheusDataplane,2023-03-29T15:26:32Z,2024-08-26T12:53:38Z,674144c8e84cb4aca3bfc448f08b88bb516d3b77,Kyle Brandt -alertStateHistoryLokiOnly,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver -alertStateHistoryLokiPrimary,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver -alertStateHistoryLokiSecondary,2023-03-30T18:53:21Z,,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver +alertStateHistoryLokiOnly,2023-03-30T18:53:21Z,2025-04-08T13:50:27Z,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver +alertStateHistoryLokiPrimary,2023-03-30T18:53:21Z,2025-04-08T13:50:27Z,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver +alertStateHistoryLokiSecondary,2023-03-30T18:53:21Z,2025-04-08T13:50:27Z,b2abb6328677a90e3f3bc8b706d22734696d02e3,Alexander Weaver unifiedRequestLog,2023-03-31T13:38:09Z,,be9361cb9ec2f1ad0bc0cda139c56e3cd46deb16,Emil Tullstedt prometheusResourceBrowserCache,2023-04-03T14:07:17Z,2023-10-11T13:18:56Z,96e9e807396b600c3325f4d3580eeef0710f6050,Galen Kistler renderAuthJWT,2023-04-03T16:53:38Z,,87a0c95164074dce6a877ea3faa3c7ac0e705ad3,Joan López de la Franca Beltran @@ -185,14 +185,14 @@ awsAsyncQueryCaching,2023-07-21T15:34:07Z,,56913fbd9578c4666599b2ab46423361d6c92 toggleLabelsInLogsUI,2023-07-24T08:22:47Z,2023-10-27T11:00:49Z,84f94cdc24c3b911ae4f8eabfe875b3e4a9c1f1b,Matias Chomicki azureMonitorDataplane,2023-07-24T13:50:49Z,2023-11-08T15:02:46Z,ee60d8c82dbcc8496d52d673bfc000901d72f232,Kyle Brandt gcomOnlyExternalOrgRoleSync,2023-07-25T12:27:02Z,2023-11-13T09:56:02Z,f7c6491f7317f3fbaf4f21b8beec967d76778a87,Ieva -traceQLStreaming,2023-07-26T13:33:16Z,,89092a1e697788428f76fead65371b7c904cd13f,Andre Pereira +traceQLStreaming,2023-07-26T13:33:16Z,2025-04-09T12:57:07Z,89092a1e697788428f76fead65371b7c904cd13f,Andre Pereira prometheusConfigOverhaulAuth,2023-07-26T16:09:53Z,2025-01-02T21:19:11Z,98cb3ce3b63ee4d1b82e6b04d11f8a6a065de7b4,Brendan O'Handley configurableSchedulerTick,2023-07-26T16:44:12Z,,c7598cc6fb492cb7cc34eec90fbd2a08d859cffd,Yuri Tseretyan permissionsFilterRemoveSubquery,2023-08-02T07:39:25Z,,2c26a02b82238e0f89ca08844914cff22a84255a,Sofia Papagiannaki influxdbSqlSupport,2023-08-02T16:27:43Z,2024-04-18T14:29:27Z,77e7ae2a1b253276ceae4ef5cda1dffc0e674b4d,ismail simsek noBasicRole,2023-08-04T09:08:14Z,2023-10-12T11:00:26Z,64ed77ddce79009ce05f43801cc578c135f05b6b,linoman -alertingNoDataErrorExecution,2023-08-15T14:27:15Z,,0717ec11d6042bbc5b4cb8d908a396ecec78cdd5,Yuri Tseretyan -metricsSummary,2023-08-28T14:02:12Z,,59e4c257bba00968741ffc235e2dbe9d4c5b90ac,Joey +alertingNoDataErrorExecution,2023-08-15T14:27:15Z,2025-03-14T18:51:58Z,0717ec11d6042bbc5b4cb8d908a396ecec78cdd5,Yuri Tseretyan +metricsSummary,2023-08-28T14:02:12Z,2025-03-24T09:17:32Z,59e4c257bba00968741ffc235e2dbe9d4c5b90ac,Joey angularDeprecationUI,2023-08-29T14:05:47Z,,bf4d025012af42ce023b1166e95626f86e819855,Giuseppe Guerra dashgpt,2023-08-30T20:22:05Z,,2f4fbf89cac96b74c2375b9ec9710a2c890dcf8f,Nathan Marrs reportingRetries,2023-08-31T07:47:47Z,,f919c55bbb417f3b1023d72bbfce6617acfd1ab9,Agnès Toulet @@ -212,7 +212,7 @@ cloudWatchWildCardDimensionValues,2023-09-27T14:41:48Z,2024-03-15T13:49:53Z,06a3 externalServiceAccounts,2023-09-28T07:26:37Z,,4563fc48afe81ff1af54b6a5bf6d65355f961155,Gabriel MABILLE alertingModifiedExport,2023-09-28T14:07:45Z,2023-10-12T17:17:32Z,169c5262a5e7d04234119a3733b00bb787b8bf90,Sonia Aguilar enableNativeHTTPHistogram,2023-10-03T18:23:55Z,,0fc403d116b9cbd0b93e497cff0ddded55f9fe60,Carl Bergquist -transformationsVariableSupport,2023-10-04T14:28:46Z,,40cdb3033697e8d8e43d0ee0342f0d16fac7d633,Oscar Kilhed +transformationsVariableSupport,2023-10-04T14:28:46Z,2025-02-17T12:07:11Z,40cdb3033697e8d8e43d0ee0342f0d16fac7d633,Oscar Kilhed kubernetesPlaylists,2023-10-05T19:00:36Z,2024-08-13T08:03:28Z,664ebf771e2a1d2f94bc14d3dababb634c4ac40f,Todd Treece grafanaAPIServerWithExperimentalAPIs,2023-10-06T18:55:22Z,,717a9dd6160e352ff7c184bfcddf2410eed9d908,Ryan McKinley panelMonitoring,2023-10-09T05:19:08Z,,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin @@ -225,9 +225,10 @@ panelTitleSearchInV1,2023-10-13T12:04:24Z,2025-01-21T09:59:32Z,bf2f2540da7a4e4b8 exploreContentOutline,2023-10-13T16:57:13Z,2024-06-24T15:45:42Z,4ec54bc2c39ba43843c693fdb2a4529b6a4703f2,Haris Rozajac formatString,2023-10-13T18:17:12Z,,889576ac1d9278b1c6e3e278e8195968646a2db0,Sol pluginsInstrumentationStatusSource,2023-10-17T08:27:45Z,2024-02-21T11:57:40Z,f5076d1868caa14ce44a70e812315541b4199d9f,Giuseppe Guerra -teamHttpHeaders,2023-10-17T10:23:54Z,,be5ba6813209b5b24e955e0f761032cb5826b578,Eric Leijonmarck +teamHttpHeaders,2023-10-17T10:23:54Z,2025-02-20T10:26:46Z,be5ba6813209b5b24e955e0f761032cb5826b578,Eric Leijonmarck costManagementUi,2023-10-17T16:15:51Z,2024-01-08T14:25:11Z,de1ed216f4bbf6f341aa22b144fa66d583a63981,Adam Bannach -managedPluginsInstall,2023-10-18T13:17:03Z,,43add83d1a84fe0945860a9cc700df92c7e8341f,Hugo Kiyodi Oshiro +managedPluginsInstall,2023-10-18T13:17:03Z,2025-02-17T15:07:41Z,43add83d1a84fe0945860a9cc700df92c7e8341f,Hugo Kiyodi Oshiro +prometheusPromQAIL,2023-10-19T15:45:32Z,2025-02-21T10:33:12Z,5580d061019bee46ea2e69c94041f3da14585ceb,Brendan O'Handley cloudWatchBatchQueries,2023-10-20T19:09:41Z,,ecbc52f51529e1f35e26895db1a10f8a1c2f4244,Isabella Siu alertingContactPointsV2,2023-10-25T13:57:53Z,2023-11-30T12:37:14Z,e12e40fc2493160338237b0b94e72fa530a78ef4,Gilles De Mey alertmanagerRemoteOnly,2023-10-30T16:27:08Z,,363830883cb1f5de30f7015df5cba419df47468e,Santiago @@ -276,18 +277,18 @@ onPremToCloudMigrations,2024-01-22T16:09:08Z,,cf13cb9f70c2230f17450667ce59440304 alertingSaveStatePeriodic,2024-01-23T16:03:30Z,,aa25776f813926cb4f1947d4ae5a014f4e7728ff,Jean-Philippe Quéméner promQLScope,2024-01-29T20:22:17Z,,43d0664340f3e3af219d7b5c747f486a073f5ce3,Kyle Brandt slateAutocomplete,2024-01-31T10:01:20Z,2024-02-01T13:03:11Z,39057552dc2ddfe8ee770c67a71cf55159aca741,Ashley Harrison -nodeGraphDotLayout,2024-01-31T16:26:12Z,,cb945aa5df3453ab2a851ef7ec8bee244e3eeac4,Andrej Ocenas +nodeGraphDotLayout,2024-01-31T16:26:12Z,2025-04-08T14:37:17Z,cb945aa5df3453ab2a851ef7ec8bee244e3eeac4,Andrej Ocenas kubernetesQueryServiceRewrite,2024-01-31T18:36:51Z,2024-04-19T09:26:21Z,e013cd427cb0457177e11f19ebd30bc523b36c76,Ryan McKinley influxdbRunQueriesInParallel,2024-02-01T10:58:24Z,,536153c3362782ae7f79e06755e8c7e0cd3b3acd,ismail simsek groupToNestedTableTransformation,2024-02-07T14:28:26Z,,756cd3c28b0648f55a6c0158f5faf63b632ebe70,Kyle Cunningham newPDFRendering,2024-02-08T12:09:34Z,,28e66b4ad82ecebb551374325f0be4332412341c,Agnès Toulet -autoMigrateGraphPanel,2024-02-08T22:00:48Z,,829672759c12b27f849c92c3a2aee4a6b0037920,Nathan Marrs +autoMigrateGraphPanel,2024-02-08T22:00:48Z,2025-04-04T09:31:35Z,829672759c12b27f849c92c3a2aee4a6b0037920,Nathan Marrs dashboardSceneSolo,2024-02-11T08:08:47Z,,fe6d1460b09b403fc74fd20e95f61513eede2555,Torkel Ödegaard kubernetesAggregator,2024-02-12T20:59:35Z,,d6e6298103d5d6a4efd1c21a3d74f429b506f3a7,Todd Treece -autoMigratePiechartPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs -autoMigrateStatPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs -autoMigrateTablePanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs -autoMigrateWorldmapPanel,2024-02-14T16:06:25Z,,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs +autoMigratePiechartPanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs +autoMigrateStatPanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs +autoMigrateTablePanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs +autoMigrateWorldmapPanel,2024-02-14T16:06:25Z,2025-04-04T09:31:35Z,ce750e06187599da6b9c0a91ef95c7a62fe0d069,Nathan Marrs groupByVariable,2024-02-14T17:18:04Z,,f016f95298fe490a865612864520f3622f8e804a,Dominik Prokop alertingUpgradeDryrunOnStart,2024-02-16T16:29:54Z,2024-03-14T14:36:35Z,dfaf6d1e2e13b2bd11dc8f0cd4432bfcad819aa9,Matthew Jacobson expressionParser,2024-02-17T00:59:11Z,,f23f50f58d7ab5cb1fd88b42b6c58ec09c1a159d,Ryan McKinley @@ -301,12 +302,12 @@ publicDashboardsScene,2024-03-22T14:48:21Z,,8d4ca72f2a0e66c446d58d8bf13fadbc988f autoMigrateXYChartPanel,2024-03-22T15:44:37Z,2024-11-14T16:36:18Z,d7fa99e2df8269425eefb6373a665d8ec0b219d9,Leon Sorokin usePrometheusFrontendPackage,2024-03-23T00:47:53Z,2024-04-15T21:45:23Z,d0845952117152e7707f964cc343790497220990,Brendan O'Handley oauthRequireSubClaim,2024-03-25T13:22:24Z,,2f3a01f79fda7f6c465dd64adf5cf5c7227f9d19,Karl Persson -authAPIAccessTokenAuth,2024-04-02T15:45:15Z,,5340a6e548b1e5fcd3af66695ca34d7e943fd068,Jo +authAPIAccessTokenAuth,2024-04-02T15:45:15Z,2025-02-04T15:31:24Z,5340a6e548b1e5fcd3af66695ca34d7e943fd068,Jo newDashboardWithFiltersAndGroupBy,2024-04-04T11:25:21Z,,32b6ef9d153dc7def2c17b81b02cfb1c412eb315,Dominik Prokop prometheusCodeModeMetricNamesSearch,2024-04-04T20:38:23Z,,559fab9dc6a6c882e7d9621ba013c3a53347176a,Nick Richmond cloudWatchNewLabelParsing,2024-04-05T15:57:56Z,,58f32150c262605d188874b88f44a7dece4b9388,Isabella Siu -exploreMetrics,2024-04-09T18:15:18Z,,66c0fd4dcc3202e11f41b302d27894dc162fb288,Darren Janeczek -accessActionSets,2024-04-12T16:19:25Z,,56f4664875047d6861ea3facbc94cd921e263950,Ieva +exploreMetrics,2024-04-09T18:15:18Z,2025-04-11T20:45:14Z,66c0fd4dcc3202e11f41b302d27894dc162fb288,Darren Janeczek +accessActionSets,2024-04-12T16:19:25Z,2025-03-13T15:18:23Z,56f4664875047d6861ea3facbc94cd921e263950,Ieva disableNumericMetricsSortingInExpressions,2024-04-16T14:52:47Z,,d3fee607e2818747ad02daf6b8c58cc801075076,Nick Richmond queryService,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley queryServiceFromUI,2024-04-19T09:26:21Z,,5a8384a2455bbd3c0ba5ec67e5f5e3cc4a836904,Ryan McKinley @@ -319,7 +320,8 @@ tlsMemcached,2024-05-09T19:12:08Z,,b009536329d110afd807ef2f27f2b7dcc7d310ba,lean notificationBanner,2024-05-13T09:32:34Z,2025-01-10T10:18:43Z,f3953b4955c218cc4678e842faa3d3380fd0f8f7,Alex Khomenko dualWritePlaylistsMode2,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R dualWritePlaylistsMode3,2024-05-14T12:11:56Z,2024-05-31T18:18:09Z,6836bfe1ea1bf62f4eb66dc1328a456183874f81,Arati R -datasourceProxyDisableRBAC,2024-05-21T13:05:16Z,,0072e4a92d896df343d9586522d6f7533773da78,Aaron Godin +dashboardRestore,2024-05-16T17:36:26Z,2025-04-03T07:52:54Z,42d75ac737d7ac001a6d53376e25512408a01db5,Ezequiel Victorero +datasourceProxyDisableRBAC,2024-05-21T13:05:16Z,2025-02-25T17:30:58Z,0072e4a92d896df343d9586522d6f7533773da78,Aaron Godin alertingDisableSendAlertsExternal,2024-05-23T12:29:19Z,,8421919cb552b9e8dbd4ebba20bcc67bbc5e6b4f,Steve Simpson datasourceQueryTypes,2024-05-23T16:46:28Z,,42b0f802de31f3d42ece68544c193a69ea381bc0,Ryan McKinley alertingListViewV2,2024-05-24T14:40:49Z,,99b5259cc1efa3f5c9dbb9b0153be14437818f38,Gilles De Mey @@ -330,7 +332,7 @@ kubernetesDashboards,2024-06-05T14:34:23Z,,41e0430f83bf7db50c4caaa1472afa3bf3d5c azureMonitorPrometheusExemplars,2024-06-06T16:53:17Z,,c9778c3332aa93e5dd8bc3b894264e1955f0d593,Andreas Christou pinNavItems,2024-06-10T11:40:03Z,,84b638fb26cecf856374bb3d09b123061b4b8a6b,Laura Fernández authZGRPCServer,2024-06-13T09:41:35Z,,afcb5a855c26e985e43861bff6fab36b1b008109,Gabriel MABILLE -openSearchBackendFlowEnabled,2024-06-17T09:41:50Z,,ab2af9b8f75cd13595f4d487c1168e849768a518,Ida Štambuk +openSearchBackendFlowEnabled,2024-06-17T09:41:50Z,2025-02-03T13:24:49Z,ab2af9b8f75cd13595f4d487c1168e849768a518,Ida Štambuk ssoSettingsLDAP,2024-06-18T11:31:27Z,,d074cc7892b96a1333bd07011baff146ea71e21d,Mihai Doarna databaseReadReplica,2024-06-18T15:07:15Z,2024-09-25T23:21:39Z,50244ed4a1435cbf3e3c87d4af34fd7937f7c259,Kristin Laemmert disableClassicHTTPHistogram,2024-06-18T19:37:44Z,,3bbc821131f1b10ace139dbb4a6880fb77686646,Dave Henderson @@ -344,7 +346,7 @@ bodyScrolling,2024-07-01T10:28:39Z,2024-09-24T12:23:18Z,c0058f9c7e390d8a196f5b37 cloudwatchMetricInsightsCrossAccount,2024-07-02T10:34:12Z,2025-01-10T22:23:23Z,36ff0fe63a7710eb496f2f048feb71e6eb6e3c56,Ida Štambuk dataplaneAggregator,2024-08-09T08:41:07Z,,122e291134c689ff57eae4461cd5953914b5a36c,Todd Treece adhocFilterOneOf,2024-08-12T08:56:42Z,2024-09-05T12:49:24Z,ab3e8652aa865e43a3f2c164b626c42dfffab33e,Ashley Harrison -prometheusRunQueriesInParallel,2024-08-12T12:31:39Z,,c9ddc688a2b2c4ebb49e8ecf71dc01e33704da32,Vijay Samuel +prometheusRunQueriesInParallel,2024-08-12T12:31:39Z,2025-04-11T22:11:19Z,c9ddc688a2b2c4ebb49e8ecf71dc01e33704da32,Vijay Samuel backgroundPluginInstaller,2024-08-12T14:39:31Z,2024-09-23T13:49:18Z,d342e76f636e3a5751c5e7baccd1c8910b50d863,Andres Martinez Gotor pluginsDetailsRightPanel,2024-08-13T09:55:30Z,,8044cb50f17a021a32e7ac0b83cf8d723457a9ae,Yulia Shanyrova lokiSendDashboardPanelNames,2024-08-22T19:30:43Z,,ec857e1de99d228668e3fb2c0bfd30230a96e180,Sven Grossmann @@ -355,8 +357,10 @@ exploreLogsLimitedTimeRange,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa976 exploreLogsShardSplitting,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa9766a995b5711,Sven Grossmann newFiltersUI,2024-08-30T12:48:13Z,,00ae49a61adff11e5fbac3341941b35185d9e8d9,Sergej-Vlasov appPlatformAccessTokens,2024-09-05T16:18:44Z,2024-10-14T10:47:18Z,d5ebaa0ef92edecfb9511dc7c8080be25f8cc7e7,Claudiu Dragalina-Paraipan -vizActions,2024-09-09T14:11:55Z,,af48d3db1eb2d8681843f5997e50fea5e5ea3096,Adela Almasan +appSidecar,2024-09-09T12:45:05Z,2025-04-10T20:04:12Z,5e2ac24890906e5070323d87730dd78a4f885963,Andrej Ocenas +vizActions,2024-09-09T14:11:55Z,2025-02-26T23:15:01Z,af48d3db1eb2d8681843f5997e50fea5e5ea3096,Adela Almasan groupAttributeSync,2024-09-09T15:29:43Z,,6ded6a8872204a818b3795dc733cc5fe5db066a0,Aaron Godin +kubernetesFolders,2024-09-10T09:22:08Z,2025-01-23T14:25:03Z,b12a29a1dac8b9aec4a99be08e1665939cb27dc5,Arati R. alertingFilterV2,2024-09-11T11:29:26Z,,90ee52e8d9c14237f8a57b622c0def7512e657cd,Gilles De Mey improvedExternalSessionHandling,2024-09-17T10:54:39Z,,41cd0f51800d4849345fc0980ca4173967fc8e9e,Misi datasourceAPIServers,2024-09-19T08:28:27Z,,f21a5987a22bcdb596d6a258d2960e4151348b63,Ryan McKinley @@ -366,7 +370,7 @@ alertingQueryAndExpressionsStepMode,2024-09-26T06:33:14Z,,536edee7bff0e393054775 rolePickerDrawer,2024-09-26T12:51:38Z,,e2816ee51a1497f1cced62584a026fc8121b59a0,linoman alertingPrometheusRulesPrimary,2024-09-27T12:27:16Z,,db42af20ca8875fbe3410ddd49e0effba6d8594a,Konrad Lalik unifiedStorageSearch,2024-09-30T19:46:14Z,,6a3eb276efb8ba7a2ccb9cb79977e2def76d406d,owensmallwood -grafanaAPIServerTestingWithExperimentalAPIs,2024-10-03T10:11:40Z,,a42caa7a61ffb0c2694ff294af3403913e4fc103,Arati R. +grafanaAPIServerTestingWithExperimentalAPIs,2024-10-03T10:11:40Z,2025-01-23T14:25:03Z,a42caa7a61ffb0c2694ff294af3403913e4fc103,Arati R. pluginsSriChecks,2024-10-04T12:55:09Z,,0db65d229e36b78802c1e8bd0713ac44e7a7cdc7,Giuseppe Guerra onPremToCloudMigrationsAlerts,2024-10-07T10:53:24Z,2024-12-17T11:56:18Z,712314e8324fd86ec33ecb840f868eb1f1cac154,Matheus Macabu appPlatformGrpcClientAuth,2024-10-14T10:47:18Z,,a69ee676babc7644da41781efc5ae2c301f06de6,Claudiu Dragalina-Paraipan @@ -380,21 +384,21 @@ azureMonitorDisableLogLimit,2024-10-24T13:32:09Z,,2a65de8aaa98ddf5db9f69afe0cfb1 reloadDashboardsOnParamsChange,2024-10-25T12:56:54Z,,97c0ff2ae467452aa2ea1619b6e014bc005e09fa,Bogdan Matei dashboardSchemaV2,2024-10-29T10:35:18Z,2024-12-19T12:28:20Z,f062c66f89b09fca7716438ac488a76bf81ed7f8,Alexa V playlistsWatcher,2024-11-01T17:47:24Z,2024-12-20T03:09:31Z,403b60723d9960b29c1ad9b1fd5b22b3bc340bef,Todd Treece -sqlQuerybuilderFunctionParameters,2024-11-04T16:13:35Z,,85c696c4ad5d7e29fa6d66e0239a0efa653ffdd2,Zoltán Bedi +sqlQuerybuilderFunctionParameters,2024-11-04T16:13:35Z,2025-02-19T14:38:27Z,85c696c4ad5d7e29fa6d66e0239a0efa653ffdd2,Zoltán Bedi enableExtensionsAdminPage,2024-11-05T15:55:10Z,,c7a7f7dce5d93dc5044c4be140433ee2d6150fb9,Marcus Andersson exploreMetricsRelatedLogs,2024-11-05T16:28:43Z,,82ac9e2bb67a665c3b56e3a80c47d8aea7f7d044,ismail simsek enableScopesInMetricsExplore,2024-11-06T13:11:33Z,,a80517d6fecf245cb9366b5662ab6d8760200389,Bogdan Matei -zipkinBackendMigration,2024-11-07T09:35:53Z,,c151021b162d99da2cb0f09485f90a2b5d38c11c,Ivana Huckova +zipkinBackendMigration,2024-11-07T09:35:53Z,2025-01-28T14:11:10Z,c151021b162d99da2cb0f09485f90a2b5d38c11c,Ivana Huckova preinstallAutoUpdate,2024-11-07T12:14:25Z,,a415c0b83184ad473b3f8e10c0319c46d9e5c515,Andres Martinez Gotor enableSCIM,2024-11-07T14:38:46Z,,3d51ca7377eff46b39750a8e0311ead7b75fd359,linoman logQLScope,2024-11-11T11:53:24Z,,c7b6822a5e9cba703e521536947682aaf801203e,Carl Bergquist -userStorageAPI,2024-11-12T11:56:41Z,,c3494614e39638f6d78b79397cf6032cbcde4b9f,Andres Martinez Gotor +userStorageAPI,2024-11-12T11:56:41Z,2025-03-27T12:40:00Z,c3494614e39638f6d78b79397cf6032cbcde4b9f,Andres Martinez Gotor crashDetection,2024-11-12T15:07:27Z,,3a6858cf2602207da6e351f7fa6beb1d18fd35ad,Piotr Jamróz passwordlessMagicLinkAuthentication,2024-11-14T13:50:55Z,,6abe99efd64b8867112d9d9c74971d96840ce32d,colin-stuart reportingUseRawTimeRange,2024-11-14T20:08:03Z,,ec1a722504b5c09ba01b96b95907a3bdd1c0b106,Juan Cabanas jaegerBackendMigration,2024-11-15T14:40:20Z,,cc1d76fc0aabf229605451d015e75ebd6bae2a0c,Gareth Dawson alertingUIOptimizeReducer,2024-11-18T10:59:00Z,,76444c7913851303d8fae61046d8c2b0d858080d,Sonia Aguilar -onPremToCloudMigrationsAuthApiMig,2024-11-21T18:46:06Z,,e9fae5bd7fe1bd5194caeaf1d0180cbba09e08a0,lean.dev +onPremToCloudMigrationsAuthApiMig,2024-11-21T18:46:06Z,2025-01-24T16:53:58Z,e9fae5bd7fe1bd5194caeaf1d0180cbba09e08a0,lean.dev provisioning,2024-11-22T09:03:50Z,,53245e274288caac8b1599e74184f40578a996fb,Ryan McKinley alertingNotificationsStepMode,2024-11-22T11:07:45Z,,977184b878f4ba0c5ef19eb6c2ba43bcbefeceb8,Sonia Aguilar scopeApi,2024-11-27T07:58:25Z,,21bd47e512180001e4dfb81cc04553bdd56f8789,Carl Bergquist @@ -403,7 +407,8 @@ feedbackButton,2024-12-02T17:08:15Z,,8a1b89a5ebb847f6b29e92dcea796f00c30431d6,Mi elasticsearchCrossClusterSearch,2024-12-12T22:20:04Z,,b3a12f486eba69e20dd7ff3a3d4dd065ede7a99f,Isabella Siu unifiedHistory,2024-12-13T10:41:18Z,,aac62c89dae1092836a91d1b6ae6bd7127fe676a,Laura Fernández lokiLabelNamesQueryApi,2024-12-13T14:31:41Z,,5ac7443fcec0db412d3333044a82c2c26b5aece7,Sven Grossmann -useV2DashboardsAPI,2024-12-17T21:17:09Z,,070f0e4457c5967102ef157197073dc2662f6fb8,Dominik Prokop +kubernetesCliDashboards,2024-12-13T22:55:43Z,2025-02-18T23:11:26Z,8f6e9f8ed0a5024a510cc337c9f1e6972bfb23d4,Stephanie Hingtgen +useV2DashboardsAPI,2024-12-17T21:17:09Z,2025-03-12T17:43:32Z,070f0e4457c5967102ef157197073dc2662f6fb8,Dominik Prokop investigationsBackend,2024-12-18T08:31:03Z,,f46c07aba7b6faccd2ecafc83051d1410cacc867,Jackson Coelho unifiedStorageSearchSprinkles,2024-12-18T17:00:54Z,,4837585cab0fd84184a8c6f5d6891f442a2b95f1,owensmallwood prometheusSpecialCharsInLabelValues,2024-12-18T21:31:08Z,,721c50a304588ebd7cea76e301ec0f68a5a55d68,Nick Richmond @@ -411,13 +416,59 @@ unifiedStorageSearchUI,2024-12-19T18:21:48Z,,a8f347144ddc16f2033fdeb4f3474e49239 playlistsReconciler,2024-12-20T03:09:31Z,,24bf337c562dc9b9d8684cc9acb7ea171ea83414,Charandas k8SFolderCounts,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,Leonor Oliveira k8SFolderMove,2024-12-27T17:10:44Z,,df36e77cd31d2ad77e3d708748d040367a0c8c9c,Leonor Oliveira +kubernetesRestore,2025-01-03T14:48:47Z,2025-03-20T21:38:32Z,5429512779bd5f25b88ff728ea91efdef7dfafa0,Stephanie Hingtgen improvedExternalSessionHandlingSAML,2025-01-09T17:02:49Z,,c52ec21c75ab72c2f7d28259bac0364edae560d0,Misi teamHttpHeadersMimir,2025-01-13T10:42:47Z,,04acbcdef23f673bd6bbfdbbece29c9769ce155a,Eric Leijonmarck ABTestFeatureToggleA,2025-01-13T21:13:13Z,,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs ABTestFeatureToggleB,2025-01-13T21:13:13Z,,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs -queryLibraryDashboards,2025-01-14T11:01:15Z,,740cd22fe51a3543c182857f31fc97fd42263306,Ashley Harrison +kubernetesFoldersServiceV2,2025-01-13T21:15:35Z,2025-02-18T23:11:26Z,766d645d827f5e6e0872ae30e5fe23226ae85785,maicon +queryLibraryDashboards,2025-01-14T11:01:15Z,2025-02-14T16:39:22Z,740cd22fe51a3543c182857f31fc97fd42263306,Ashley Harrison elasticsearchImprovedParsing,2025-01-15T17:05:54Z,,bab55a4cb84f2ba57838f96a492ab9aa7f307957,Adam Yeats grafanaAdvisor,2025-01-20T10:08:00Z,,c1364d6be6f552203ba786f17a89664304b89247,Andres Martinez Gotor datasourceConnectionsTab,2025-01-21T17:39:48Z,,97d8f68b705f9949493079d1833abfe80e7b48f3,Syerikjan Kh unifiedStorageSearchPermissionFiltering,2025-01-22T11:38:37Z,,dd483fc17fa4a2931848e3574cfc31ea6f6530d9,owensmallwood -kubernetesClientDashboardsFolders,2025-02-18T21:15:35Z,,766d645d827f5e6e0872ae30e5fe23226ae85785,Stephanie Hingtgen +alertingSaveStateCompressed,2025-01-27T17:47:33Z,,cb43f4b6962fca18655b3ba634adeb4d59dc89df,Alexander Akhmetov +fetchRulesUsingPost,2025-01-29T12:17:44Z,,1444051b65af0de6c412a12132083135c7730414,Fayzal Ghantiwala +templateVariablesUsesCombobox,2025-01-31T09:53:13Z,,7190bfb0ca675fc1b3b5d7ddf7e0ee9d1c9ca3d7,Tobias Skarhed +alertingAlertmanagerExtraDedupStage,2025-01-31T16:12:38Z,2025-03-26T19:15:10Z,0be6e1bb86613b6466e531119fe1cfec67d35aac,Yuri Tseretyan +alertingAlertmanagerExtraDedupStageStopPipeline,2025-01-31T16:12:38Z,2025-03-26T19:15:10Z,0be6e1bb86613b6466e531119fe1cfec67d35aac,Yuri Tseretyan +exploreMetricsUseExternalAppPlugin,2025-02-03T20:46:54Z,2025-04-11T20:45:14Z,29fa6dfc8de0a758f9f37f000c923f0806c3b629,Nick Richmond +newLogsPanel,2025-02-04T17:40:17Z,,ff926c5ac5daa760378ce233d7d9f876f56727b5,Matias Chomicki +grafanaconThemes,2025-02-06T11:08:04Z,,7d3a77a45c4a48582892a95b60b160b69543e51f,Ashley Harrison +pluginsCDNSyncLoader,2025-02-07T10:07:08Z,,ccb9cab1318ea76ebb6b1005e7a53e113cba0d3b,Giuseppe Guerra +alertingConversionAPI,2025-02-12T07:13:21Z,2025-04-05T08:27:02Z,9593e51da7c05ed548f11d24ac4c667c51dbe6bb,Alexander Akhmetov +alertingJiraIntegration,2025-02-14T12:22:04Z,,af8cab92109ecfbb54625ff25a3b90a4f92dc46a,Sonia Aguilar +alertingRuleVersionHistoryRestore,2025-02-17T12:25:32Z,,2014d27defe5668ba07913cec6f2186c90eaaab2,Sonia Aguilar +newShareReportDrawer,2025-02-17T19:05:46Z,,9df6412e92559f2329c2baac0025f56fc57c5bf9,Ezequiel Victorero +kubernetesClientDashboardsFolders,2025-02-18T23:11:26Z,,3e6f40c87386984be60e391b26a7559e3469dac3,Stephanie Hingtgen +managedDualWriter,2025-02-19T14:50:39Z,,5a40c84568485da55ccb42998c6d291d310abd56,Ryan McKinley +rendererDisableAppPluginsPreload,2025-02-24T14:43:06Z,,608d974585c696253ac629f3c7bfc3a0043cbd49,Agnès Toulet +assetSriChecks,2025-03-04T10:56:35Z,,bbfeb8d220cc67c329aa2b5d6ed693ae1cb54325,Jack Westbrook +alertRuleRestore,2025-03-05T14:15:26Z,,374380d1f6ab05e98bc00b0b0fdaa076d1d30ffa,Yuri Tseretyan +grafanaManagedRecordingRulesDatasources,2025-03-07T13:30:40Z,,14ebec527ccf22c15321ac8394a4f9c3909dd4e1,Steve Simpson +inviteUserExperimental,2025-03-07T19:09:59Z,,5e21b9e2d1ebad0dcb5fce5fa1bc48b74cea4b45,Juan Cabanas +extraLanguages,2025-03-11T10:07:16Z,2025-03-27T12:58:52Z,210c886bb7b913e4ca0b77ff142a72185a82a385,Josh Hunt +infinityRunQueriesInParallel,2025-03-14T12:54:04Z,,d72b6649810ffacebe73ef67c6b5bd6e46dfab47,ismail simsek +noBackdropBlur,2025-03-14T15:21:35Z,2025-04-08T10:58:19Z,bf172dfd296d15c02473904646c19d2bb663cc3f,Josh Hunt +alertingMigrationUI,2025-03-14T16:40:05Z,,ef9dca9ea369997116cd7880c238597fc18ca682,Tom Ratcliffe +unifiedStorageHistoryPruner,2025-03-17T10:36:38Z,,1700a8aa9f2a103f953a385544c2ce200e6fcf9d,Jean-Philippe Quéméner +secretsManagementAppPlatform,2025-03-19T09:25:14Z,,ac4b2a320071323f0d2849b844f7e8c3afbff21d,Matheus Macabu +unifiedStorageGrpcConnectionPool,2025-03-21T13:24:54Z,,ba3e8014b3363762369c434c0c67d871a311a033,Jean-Philippe Quéméner +tableNextGen,2025-03-26T03:57:57Z,,03d6d8f854ac06701bba2d5d10bedd8a227bea4e,Drew Slobodnjak +alertingRuleRecoverDeleted,2025-03-27T14:39:26Z,,f9471ac10b65e08343b08d82e1209f1ae7006955,Sonia Aguilar +localizationForPlugins,2025-03-31T04:38:38Z,,18ae5d7f0c599bf417dc68a67fa6852b6cf54600,Hugo Häggmark +localeFormatPreference,2025-03-31T13:59:07Z,,4ad0492d3dc8ee81b388384e3e6c329746f46df7,Laura Fernández +useScopesNavigationEndpoint,2025-03-31T15:20:00Z,,c321afdeb710db302558ac63a8f0569c4706d003,Tobias Skarhed +xrayApplicationSignals,2025-04-01T14:42:02Z,,1aea65f6d5aae0ea9d54a6d943b27e1004f4ef85,Isabella Siu +queryServiceFromExplore,2025-04-02T10:00:33Z,,135fbf6258d85382a3c80b696b4e410fd6efe809,Gábor Farkas +azureMonitorLogsBuilderEditor,2025-04-02T14:15:25Z,,3b73ebb21096f69f4dcb287896e2cbfec65e48d7,Alyssa (Bull) Joyner +multiTenantTempCredentials,2025-04-02T20:25:50Z,,6a699b69bace2aecfe3631a5e050cda69180b97f,Isabella Siu +extensionSidebar,2025-04-03T10:16:35Z,,f27790268254294be7c09832e9a73676fd9a629c,Sven Grossmann +alertingRulePermanentlyDelete,2025-04-03T11:18:25Z,,3450d243b95acd5bf86fbbf3b183cba4b2eb8301,Sonia Aguilar +logsPanelControls,2025-04-07T14:38:55Z,,e2a6f9a84928cfb2f958522b1f50fdf3051f51de,Matias Chomicki +metricsFromProfiles,2025-04-09T10:55:28Z,,ceed8243784500557f2686bb478695cc125a0e19,Piotr Jamróz +unifiedNavbars,2025-04-09T12:51:22Z,,24474dcb9cd0d1d899dafd110a07307a9b794074,Torkel Ödegaard +dashboardDisableSchemaValidationV1,2025-04-11T16:52:46Z,,95f04c79cd5176a722d528ee942107084343469e,Marco de Abreu +dashboardDisableSchemaValidationV2,2025-04-11T16:52:46Z,,95f04c79cd5176a722d528ee942107084343469e,Marco de Abreu +dashboardSchemaValidationLogging,2025-04-11T16:52:46Z,,95f04c79cd5176a722d528ee942107084343469e,Marco de Abreu +scopeSearchAllLevels,2025-04-14T07:42:16Z,,ed65c99e543ab1cecbc608fd29eeaf564dccca44,Carl Bergquist diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 432f331d2f2..4f50369af09 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -25,7 +25,6 @@ individualCookiePreferences,experimental,@grafana/grafana-backend-group,false,fa influxdbBackendMigration,GA,@grafana/partner-datasources,false,false,true influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false -prometheusRunQueriesInParallel,GA,@grafana/oss-big-tent,false,false,false lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false dataplaneFrontendFallback,GA,@grafana/observability-metrics,false,false,true disableSSEDataplane,experimental,@grafana/observability-metrics,false,false,false @@ -63,7 +62,6 @@ lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false, wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false alertingInsights,GA,@grafana/alerting-squad,false,false,true externalCorePlugins,GA,@grafana/plugins-platform-backend,false,false,false -pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,true externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false panelMonitoring,GA,@grafana/dataviz-squad,false,false,true enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false @@ -72,7 +70,10 @@ formatString,GA,@grafana/dataviz-squad,false,false,true kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true -kubernetesClientDashboardsFolders,experimental,@grafana/grafana-app-platform-squad,false,false,false +kubernetesClientDashboardsFolders,GA,@grafana/grafana-app-platform-squad,false,false,false +dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false +dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false +dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false @@ -98,7 +99,6 @@ pdfTables,preview,@grafana/sharing-squad,false,false,false ssoSettingsApi,GA,@grafana/identity-access-team,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true -exploreMetrics,GA,@grafana/observability-metrics,false,false,true alertingSimplifiedRouting,GA,@grafana/alerting-squad,false,false,false logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false @@ -203,7 +203,6 @@ templateVariablesUsesCombobox,experimental,@grafana/grafana-frontend-platform,fa ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false grafanaAdvisor,experimental,@grafana/plugins-platform-backend,false,false,false elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false -exploreMetricsUseExternalAppPlugin,GA,@grafana/observability-metrics,false,true,false datasourceConnectionsTab,privatePreview,@grafana/plugins-platform-backend,false,false,true fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false newLogsPanel,experimental,@grafana/observability-logs,false,false,true @@ -211,6 +210,7 @@ grafanaconThemes,GA,@grafana/grafana-frontend-platform,false,true,false pluginsCDNSyncLoader,experimental,@grafana/plugins-platform-backend,false,false,false alertingJiraIntegration,experimental,@grafana/alerting-squad,false,false,true useScopesNavigationEndpoint,experimental,@grafana/grafana-frontend-platform,false,false,true +scopeSearchAllLevels,experimental,@grafana/grafana-frontend-platform,false,false,false alertingRuleVersionHistoryRestore,GA,@grafana/alerting-squad,false,false,true newShareReportDrawer,experimental,@grafana/sharing-squad,false,false,false rendererDisableAppPluginsPreload,experimental,@grafana/sharing-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 4b23d0076e6..7fcbc519d18 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -111,10 +111,6 @@ const ( // Enables running InfluxDB Influxql queries in parallel FlagInfluxdbRunQueriesInParallel = "influxdbRunQueriesInParallel" - // FlagPrometheusRunQueriesInParallel - // Enables running Prometheus queries in parallel - FlagPrometheusRunQueriesInParallel = "prometheusRunQueriesInParallel" - // FlagLokiLogsDataplane // Changes logs responses from Loki to be compliant with the dataplane specification. FlagLokiLogsDataplane = "lokiLogsDataplane" @@ -263,10 +259,6 @@ const ( // Allow core plugins to be loaded as external FlagExternalCorePlugins = "externalCorePlugins" - // FlagPluginsAPIMetrics - // Sends metrics of public grafana packages usage by plugins - FlagPluginsAPIMetrics = "pluginsAPIMetrics" - // FlagExternalServiceAccounts // Automatic service account and token setup for plugins FlagExternalServiceAccounts = "externalServiceAccounts" @@ -303,6 +295,18 @@ const ( // Route the folder and dashboard service requests to k8s FlagKubernetesClientDashboardsFolders = "kubernetesClientDashboardsFolders" + // FlagDashboardDisableSchemaValidationV1 + // Disable schema validation for dashboards/v1 + FlagDashboardDisableSchemaValidationV1 = "dashboardDisableSchemaValidationV1" + + // FlagDashboardDisableSchemaValidationV2 + // Disable schema validation for dashboards/v2 + FlagDashboardDisableSchemaValidationV2 = "dashboardDisableSchemaValidationV2" + + // FlagDashboardSchemaValidationLogging + // Log schema validation errors so they can be analyzed later + FlagDashboardSchemaValidationLogging = "dashboardSchemaValidationLogging" + // FlagDatasourceQueryTypes // Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) FlagDatasourceQueryTypes = "datasourceQueryTypes" @@ -403,10 +407,6 @@ const ( // Enables infinite scrolling for the Logs panel in Explore and Dashboards FlagLogsInfiniteScrolling = "logsInfiniteScrolling" - // FlagExploreMetrics - // Enables the new Grafana Metrics Drilldown core app - FlagExploreMetrics = "exploreMetrics" - // FlagAlertingSimplifiedRouting // Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule FlagAlertingSimplifiedRouting = "alertingSimplifiedRouting" @@ -823,10 +823,6 @@ const ( // Enables less memory intensive Elasticsearch result parsing FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing" - // FlagExploreMetricsUseExternalAppPlugin - // Use the externalized Grafana Metrics Drilldown (formerly known as Explore Metrics) app plugin - FlagExploreMetricsUseExternalAppPlugin = "exploreMetricsUseExternalAppPlugin" - // FlagDatasourceConnectionsTab // Shows defined connections for a data source in the plugins detail page FlagDatasourceConnectionsTab = "datasourceConnectionsTab" @@ -855,6 +851,10 @@ const ( // Use the scopes navigation endpoint instead of the dashboardbindings endpoint FlagUseScopesNavigationEndpoint = "useScopesNavigationEndpoint" + // FlagScopeSearchAllLevels + // Enable scope search to include all levels of the scope node tree + FlagScopeSearchAllLevels = "scopeSearchAllLevels" + // FlagAlertingRuleVersionHistoryRestore // Enables the alert rule version history restore feature FlagAlertingRuleVersionHistoryRestore = "alertingRuleVersionHistoryRestore" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 542851adf3a..c3cf020be00 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -731,6 +731,30 @@ "frontend": true } }, + { + "metadata": { + "name": "dashboardDisableSchemaValidationV1", + "resourceVersion": "1744303023863", + "creationTimestamp": "2025-04-10T16:37:03Z" + }, + "spec": { + "description": "Disable schema validation for dashboards/v1", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad" + } + }, + { + "metadata": { + "name": "dashboardDisableSchemaValidationV2", + "resourceVersion": "1744303023863", + "creationTimestamp": "2025-04-10T16:37:03Z" + }, + "spec": { + "description": "Disable schema validation for dashboards/v2", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad" + } + }, { "metadata": { "name": "dashboardNewLayouts", @@ -786,6 +810,18 @@ "expression": "true" } }, + { + "metadata": { + "name": "dashboardSchemaValidationLogging", + "resourceVersion": "1744303223631", + "creationTimestamp": "2025-04-10T16:40:23Z" + }, + "spec": { + "description": "Log schema validation errors so they can be analyzed later", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad" + } + }, { "metadata": { "name": "dashgpt", @@ -1079,7 +1115,8 @@ "metadata": { "name": "exploreMetrics", "resourceVersion": "1743693517832", - "creationTimestamp": "2024-04-09T18:15:18Z" + "creationTimestamp": "2024-04-09T18:15:18Z", + "deletionTimestamp": "2025-04-11T01:41:57Z" }, "spec": { "description": "Enables the new Grafana Metrics Drilldown core app", @@ -1107,6 +1144,7 @@ "name": "exploreMetricsUseExternalAppPlugin", "resourceVersion": "1744146547481", "creationTimestamp": "2025-04-03T15:18:37Z", + "deletionTimestamp": "2025-04-11T01:41:57Z", "annotations": { "grafana.app/updatedTimestamp": "2025-04-08 21:09:07.481769 +0000 UTC" } @@ -1278,6 +1316,32 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "flagDashboardDisableSchemaValidationV1", + "resourceVersion": "1744302136118", + "creationTimestamp": "2025-04-10T16:22:16Z", + "deletionTimestamp": "2025-04-10T16:37:03Z" + }, + "spec": { + "description": "Disable schema validation for dashboards/v1", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad" + } + }, + { + "metadata": { + "name": "flagDashboardDisableSchemaValidationV2", + "resourceVersion": "1744302136118", + "creationTimestamp": "2025-04-10T16:22:16Z", + "deletionTimestamp": "2025-04-10T16:37:03Z" + }, + "spec": { + "description": "Disable schema validation for dashboards/v2", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad" + } + }, { "metadata": { "name": "formatString", @@ -1644,13 +1708,17 @@ { "metadata": { "name": "kubernetesClientDashboardsFolders", - "resourceVersion": "1743693517832", - "creationTimestamp": "2025-02-18T21:15:35Z" + "resourceVersion": "1744337414536", + "creationTimestamp": "2025-02-18T21:15:35Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-04-11 02:10:14.536012 +0000 UTC" + } }, "spec": { "description": "Route the folder and dashboard service requests to k8s", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad" + "stage": "GA", + "codeowner": "@grafana/grafana-app-platform-squad", + "expression": "true" } }, { @@ -2370,7 +2438,8 @@ "metadata": { "name": "pluginsAPIMetrics", "resourceVersion": "1743693517832", - "creationTimestamp": "2023-09-21T11:36:32Z" + "creationTimestamp": "2023-09-21T11:36:32Z", + "deletionTimestamp": "2025-04-14T09:21:29Z" }, "spec": { "description": "Sends metrics of public grafana packages usage by plugins", @@ -2515,7 +2584,8 @@ "metadata": { "name": "prometheusRunQueriesInParallel", "resourceVersion": "1743693517832", - "creationTimestamp": "2024-08-12T12:31:39Z" + "creationTimestamp": "2024-08-12T12:31:39Z", + "deletionTimestamp": "2025-04-10T18:16:18Z" }, "spec": { "description": "Enables running Prometheus queries in parallel", @@ -2831,6 +2901,23 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "scopeSearchAllLevels", + "resourceVersion": "1744370973814", + "creationTimestamp": "2025-04-10T12:49:09Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-04-11 11:29:33.814419 +0000 UTC" + } + }, + "spec": { + "description": "Enable scope search to include all levels of the scope node tree", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "secretsManagementAppPlatform", @@ -3203,14 +3290,18 @@ { "metadata": { "name": "useScopesNavigationEndpoint", - "resourceVersion": "1743693517832", - "creationTimestamp": "2025-04-03T15:18:37Z" + "resourceVersion": "1744289349951", + "creationTimestamp": "2025-04-03T15:18:37Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-04-10 12:49:09.951013 +0000 UTC" + } }, "spec": { "description": "Use the scopes navigation endpoint instead of the dashboardbindings endpoint", "stage": "experimental", "codeowner": "@grafana/grafana-frontend-platform", "frontend": true, + "hideFromAdminPage": true, "hideFromDocs": true } }, diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index d0441b2d30f..60514654d83 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -440,16 +440,6 @@ When features are slated for removal, they will be marked as Deprecated first. }, false) } - buf += ` -## Experimental feature toggles - -[Experimental](https://grafana.com/docs/release-life-cycle/#experimental) features are early in their development lifecycle and so are not yet supported in Grafana Cloud. -Experimental features might be changed or removed without prior notice. - -` + writeToggleDocsTable(func(flag FeatureFlag) bool { - return flag.Stage == FeatureStageExperimental && !flag.RequiresDevMode - }, false) - buf += ` ## Development feature toggles diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 665ba45dd81..7280303f38e 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -50,8 +50,8 @@ func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateF if err != nil { return nil, err } - - out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID) + out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID, v1.CreateOptions{ + FieldValidation: v1.FieldValidationIgnore}) if err != nil { return nil, err } @@ -106,7 +106,9 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF meta.SetFolder(*cmd.NewParentUID) } - out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID) + out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID, v1.UpdateOptions{ + FieldValidation: v1.FieldValidationIgnore, + }) if err != nil { return nil, err } diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 8e5fa5db84d..6b1d0428ed9 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -61,12 +61,14 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink }) } if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) { - generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ + provisioningNode := &navtree.NavLink{ Text: "Provisioning", Id: "provisioning", SubTitle: "View and manage your provisioning connections", Url: s.cfg.AppSubURL + "/admin/provisioning", - }) + } + + configNodes = append(configNodes, provisioningNode) } generalNode := &navtree.NavLink{ diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index fbb99ef08bd..73b14acec52 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -294,6 +294,7 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-k8s-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 1, Text: "Kubernetes"}, "grafana-dbo11y-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 2, Text: "Databases"}, "grafana-app-observability-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApplication, Text: "Application", Icon: "graph-bar"}, + "grafana-metricsdrilldown-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 1, Text: "Metrics"}, "grafana-lokiexplore-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 2, Text: "Logs"}, "grafana-exploretraces-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 3, Text: "Traces"}, "grafana-pyroscope-app": {SectionID: navtree.NavIDDrilldown, SortWeight: 4, Text: "Profiles"}, @@ -317,10 +318,6 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-csp-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightCloudServiceProviders, Icon: "cloud"}, } - if s.features.IsEnabledGlobally(featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { - s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDDrilldown, SortWeight: 1, Text: "Metrics"} - } - if s.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAdvisor) { s.navigationAppConfig["grafana-advisor-app"] = NavigationAppConfig{ SectionID: navtree.NavIDCfg, diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index fca5c63b215..fdc345a8026 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -139,7 +139,6 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere } if hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) { - drilldownChildNavLinks := s.buildDrilldownNavLinks(c) treeRoot.AddSection(&navtree.NavLink{ Text: "Drilldown", Id: navtree.NavIDDrilldown, @@ -148,7 +147,6 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere IsNew: true, SortWeight: navtree.WeightDrilldown, Url: s.cfg.AppSubURL + "/drilldown", - Children: drilldownChildNavLinks, }) } @@ -431,20 +429,14 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na contactPointsPerms := []ac.Evaluator{ ac.EvalPermission(ac.ActionAlertingNotificationsRead), ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead), - } - // With the new alerting API, we have other permissions to consider. We don't want to consider these with the old - // alerting API to maintain backwards compatibility. - if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) { - contactPointsPerms = append(contactPointsPerms, - ac.EvalPermission(ac.ActionAlertingReceiversRead), - ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), - ac.EvalPermission(ac.ActionAlertingReceiversCreate), + ac.EvalPermission(ac.ActionAlertingReceiversRead), + ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), + ac.EvalPermission(ac.ActionAlertingReceiversCreate), - ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead), - ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite), - ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete), - ) + ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead), + ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite), + ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete), } if hasAccess(ac.EvalAny(contactPointsPerms...)) { @@ -572,17 +564,3 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *n } return nil } - -func (s *ServiceImpl) buildDrilldownNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink { - drilldownChildNavs := []*navtree.NavLink{} - if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) && !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { - drilldownChildNavs = append(drilldownChildNavs, &navtree.NavLink{ - Text: "Metrics", - SubTitle: "Queryless exploration of your metrics", - Id: "explore/metrics", - Url: s.cfg.AppSubURL + "/explore/metrics", - Icon: "code-branch", - }) - } - return drilldownChildNavs -} diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index e675a47bc27..259db2bd5a9 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -381,10 +381,9 @@ func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.Featu instancesReaderRole, instancesWriterRole, notificationsReaderRole, notificationsWriterRole, alertingReaderRole, alertingWriterRole, alertingAdminRole, alertingProvisionerRole, alertingProvisioningReaderWithSecretsRole, alertingProvisioningStatus, - } - - if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { - fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole) + // k8s roles + receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, + timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole, } return service.DeclareFixedRoles(fixedRoles...) diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index ba0854a8ce2..cdba800aab8 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -188,52 +188,6 @@ func (srv AlertmanagerSrv) RoutePostGrafanaAlertingConfigHistoryActivate(c *cont return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration activated"}) } -func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, body apimodels.PostableUserConfig) response.Response { - // Remove autogenerated config from the user config before checking provenance guard and eventually saving it. - // TODO: This and provenance guard should be moved to the notifier package. - notifier.RemoveAutogenConfigIfExists(body.AlertmanagerConfig.Route) - currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), false) - // If a config is present and valid we proceed with the guard, otherwise we - // just bypass the guard which is okay as we are anyway in an invalid state. - if err == nil { - if err := srv.provenanceGuard(currentConfig, body); err != nil { - return ErrResp(http.StatusBadRequest, err, "") - } - } - if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) { - if err != nil { - // Unclear if returning an error here is the right thing to do, preventing the user from posting a new config - // when the current one is legitimately invalid is not optimal, but we need to ensure receiver - // permissions are maintained and prevent potential access control bypasses. The workaround is to use the - // various new k8s API endpoints to fix the configuration. - return ErrResp(http.StatusInternalServerError, err, "") - } - if err := srv.k8sApiServiceGuard(currentConfig, body); err != nil { - return ErrResp(http.StatusBadRequest, err, "") - } - } - err = srv.mam.SaveAndApplyAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), body) - if err == nil { - return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"}) - } - var unknownReceiverError notifier.UnknownReceiverError - if errors.As(err, &unknownReceiverError) { - return ErrResp(http.StatusBadRequest, unknownReceiverError, "") - } - var configRejectedError notifier.AlertmanagerConfigRejectedError - if errors.As(err, &configRejectedError) { - return ErrResp(http.StatusBadRequest, configRejectedError, "") - } - if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) { - return response.Error(http.StatusNotFound, err.Error(), err) - } - if errors.Is(err, notifier.ErrAlertmanagerNotReady) { - return response.Error(http.StatusConflict, err.Error(), err) - } - - return response.ErrOrFallback(http.StatusInternalServerError, err.Error(), err) -} - func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) response.Response { am, errResp := srv.AlertmanagerFor(c.GetOrgID()) if errResp != nil { diff --git a/pkg/services/ngalert/api/api_alertmanager_guards.go b/pkg/services/ngalert/api/api_alertmanager_guards.go index 21e41b60966..a2aff6c69d5 100644 --- a/pkg/services/ngalert/api/api_alertmanager_guards.go +++ b/pkg/services/ngalert/api/api_alertmanager_guards.go @@ -16,46 +16,6 @@ import ( "github.com/grafana/grafana/pkg/util/cmputil" ) -func (srv AlertmanagerSrv) provenanceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { - if err := checkRoutes(currentConfig, newConfig); err != nil { - return err - } - if err := checkTemplates(currentConfig, newConfig); err != nil { - return err - } - if err := checkContactPoints(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers); err != nil { - return err - } - if err := checkMuteTimes(currentConfig, newConfig); err != nil { - return err - } - return nil -} - -func (srv AlertmanagerSrv) k8sApiServiceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { - // Modifications to receivers via this API is tricky with new per-receiver RBAC. Assuming we restrict the API to only - // those users with global edit permissions, we would still need to consider the following: - // - Since the UIDs stored in the database for the purposes of per-receiver RBAC are generated based on the receiver - // name, we would need to ensure continuity of permissions when a receiver is renamed. This would, preferably, - // require detecting renames and updating the permissions UID in the database. - // - Editors don't have permission to manage all receiver permissions by default, only admin users do. This means that - // certain combined operations (e.g. swapping the name of receivers) would require careful handling to ensure - // that the correct permissions are maintained, and considering receivers don't have a unique identifier outside - // their name, this is non-trivial. - - // Neither of these are insurmountable, but considering this endpoint will be removed once FlagAlertingApiServer - // becomes GA, the complexity may not be worthwhile. To that end, for now we reject any request that attempts to - // update receivers, while allowing operations that add or remove receivers. - delta, err := calculateReceiversDelta(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers) - if err != nil { - return err - } - if len(delta.Updated) > 0 { - return fmt.Errorf("cannot update receivers using this API while per-receiver RBAC is enabled; either disable the `alertingApiServer` feature flag or use an API that supports per-receiver RBAC (e.g. provisioning or receivers API)") - } - return nil -} - func checkRoutes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { reporter := cmputil.DiffReporter{} options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{}), cmp.Transformer("", func(regexp amConfig.Regexp) any { diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 0da43bbd6e3..4994c916520 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -2,7 +2,6 @@ package api import ( "context" - "crypto/md5" "encoding/base64" "encoding/json" "net/http" @@ -101,90 +100,6 @@ func TestContextWithTimeoutFromRequest(t *testing.T) { } func TestAlertmanagerConfig(t *testing.T) { - sut := createSut(t) - - t.Run("assert 404 Not Found when applying config to nonexistent org", func(t *testing.T) { - rc := contextmodel.ReqContext{ - Context: &web.Context{ - Req: &http.Request{}, - }, - SignedInUser: &user.SignedInUser{ - OrgID: 12, - }, - } - request := createAmConfigRequest(t, validConfig) - - response := sut.RoutePostAlertingConfig(&rc, request) - - require.Equal(t, 404, response.Status()) - require.Contains(t, string(response.Body()), "Alertmanager does not exist for this organization") - }) - - t.Run("assert 202 when config successfully applied", func(t *testing.T) { - rc := contextmodel.ReqContext{ - Context: &web.Context{ - Req: &http.Request{}, - }, - SignedInUser: &user.SignedInUser{ - OrgID: 1, - }, - } - request := createAmConfigRequest(t, validConfig) - - response := sut.RoutePostAlertingConfig(&rc, request) - - require.Equal(t, 202, response.Status()) - }) - - t.Run("assert 202 when alertmanager to configure is not ready", func(t *testing.T) { - sut := createSut(t) - rc := contextmodel.ReqContext{ - Context: &web.Context{ - Req: &http.Request{}, - }, - SignedInUser: &user.SignedInUser{ - OrgID: 3, // Org 3 was initialized with broken config. - }, - } - request := createAmConfigRequest(t, validConfig) - - response := sut.RoutePostAlertingConfig(&rc, request) - - require.Equal(t, 202, response.Status()) - }) - - t.Run("assert config hash doesn't change when sending RouteGetAlertingConfig back to RoutePostAlertingConfig", func(t *testing.T) { - rc := contextmodel.ReqContext{ - Context: &web.Context{ - Req: &http.Request{}, - }, - SignedInUser: &user.SignedInUser{ - OrgID: 1, - }, - } - request := createAmConfigRequest(t, validConfigWithSecureSetting) - - r := sut.RoutePostAlertingConfig(&rc, request) - require.Equal(t, 202, r.Status()) - - getResponse := sut.RouteGetAlertingConfig(&rc) - require.Equal(t, 200, getResponse.Status()) - - body := getResponse.Body() - hash := md5.Sum(body) - postable, err := notifier.Load(body) - require.NoError(t, err) - - r = sut.RoutePostAlertingConfig(&rc, *postable) - require.Equal(t, 202, r.Status()) - - getResponse = sut.RouteGetAlertingConfig(&rc) - require.Equal(t, 200, getResponse.Status()) - - newHash := md5.Sum(getResponse.Body()) - require.Equal(t, hash, newHash) - }) - t.Run("when objects are not provisioned", func(t *testing.T) { t.Run("route from GET config has no provenance", func(t *testing.T) { sut := createSut(t) @@ -229,9 +144,8 @@ func TestAlertmanagerConfig(t *testing.T) { t.Run("contact point from GET config has expected provenance", func(t *testing.T) { sut := createSut(t) rc := createRequestCtxInOrg(1) - request := createAmConfigRequest(t, validConfig) - _ = sut.RoutePostAlertingConfig(rc, request) + RoutePostAlertingConfig(t, sut.mam, rc, validConfig) response := sut.RouteGetAlertingConfig(rc) body := asGettableUserConfig(t, response) @@ -347,11 +261,8 @@ func TestGetAlertmanagerConfiguration_NewSecretField(t *testing.T) { } } ` - postable := createAmConfigRequest(t, postWithoutChanges) - - res = sut.RoutePostAlertingConfig(rc, postable) - require.Equal(t, 202, res.Status()) + RoutePostAlertingConfig(t, sut.mam, rc, postWithoutChanges) // Check that the secret field "integrationKey" is now encrypted in SecureSettings. savedConfig := &apimodels.PostableUserConfig{} err = json.Unmarshal([]byte(configs[orgId].AlertmanagerConfiguration), savedConfig) @@ -400,31 +311,6 @@ func TestAlertmanagerAutogenConfig(t *testing.T) { } } - t.Run("route POST config", func(t *testing.T) { - t.Run("does not save autogen routes", func(t *testing.T) { - sut, configs := createSutForAutogen(t) - rc := createRequestCtxInOrg(1) - request := createAmConfigRequest(t, validConfigWithAutogen) - response := sut.RoutePostAlertingConfig(rc, request) - require.Equal(t, 202, response.Status()) - - compare(t, validConfigWithoutAutogen, configs[1].AlertmanagerConfiguration) - }) - - t.Run("provenance guard ignores autogen routes", func(t *testing.T) { - sut := createSut(t) - rc := createRequestCtxInOrg(1) - request := createAmConfigRequest(t, validConfigWithoutAutogen) - _ = sut.RoutePostAlertingConfig(rc, request) - - setRouteProvenance(t, 1, sut.mam.ProvStore) - request = createAmConfigRequest(t, validConfigWithAutogen) - request.AlertmanagerConfig.Route.Provenance = apimodels.Provenance(ngmodels.ProvenanceAPI) - response := sut.RoutePostAlertingConfig(rc, request) - require.Equal(t, 202, response.Status()) - }) - }) - t.Run("route GET config", func(t *testing.T) { t.Run("when admin return autogen routes", func(t *testing.T) { sut, _ := createSutForAutogen(t) @@ -691,16 +577,6 @@ func createSut(t *testing.T) AlertmanagerSrv { } } -func createAmConfigRequest(t *testing.T, config string) apimodels.PostableUserConfig { - t.Helper() - - request := apimodels.PostableUserConfig{} - err := request.UnmarshalJSON([]byte(config)) - require.NoError(t, err) - - return request -} - func createMultiOrgAlertmanager(t *testing.T, configs map[int64]*ngmodels.AlertConfiguration) *notifier.MultiOrgAlertmanager { t.Helper() @@ -849,40 +725,6 @@ var validConfigWithAutogen = `{ } ` -var validConfigWithSecureSetting = `{ - "template_files": { - "a": "template" - }, - "alertmanager_config": { - "route": { - "receiver": "grafana-default-email" - }, - "receivers": [{ - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [{ - "uid": "", - "name": "email receiver", - "type": "email", - "settings": { - "addresses": "" - } - }]}, - { - "name": "slack", - "grafana_managed_receiver_configs": [{ - "uid": "", - "name": "slack1", - "type": "slack", - "settings": {"text": "slack text"}, - "secureSettings": { - "url": "secure url" - } - }] - }] - } -} -` - var brokenConfig = ` "alertmanager_config": { "route": { @@ -947,3 +789,13 @@ func asGettableHistoricUserConfigs(t *testing.T, r response.Response) []apimodel require.NoError(t, err) return body } + +// RoutePostAlertingConfig drop-in replacement for removed POST endpoint to make test transition easier. +func RoutePostAlertingConfig(t *testing.T, mam *notifier.MultiOrgAlertmanager, rc *contextmodel.ReqContext, amConfig string) { + t.Helper() + cfg := apimodels.PostableUserConfig{} + err := json.Unmarshal([]byte(amConfig), &cfg) + require.NoError(t, err) + err = mam.SaveAndApplyAlertmanagerConfiguration(rc.Req.Context(), 1, cfg) + require.NoError(t, err) +} diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index d3bc92cd6f9..93e3b746e60 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -8,7 +8,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" ) @@ -229,9 +228,6 @@ func (api *API) authorize(method, path string) web.Handler { eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead) case http.MethodGet + "/api/alertmanager/grafana/api/v2/status": eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead) - case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/alerts": - // additional authorization is done in the request handler - eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite)) case http.MethodPost + "/api/alertmanager/grafana/config/history/{id}/_activate": eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite)) case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/receivers": @@ -296,12 +292,9 @@ func (api *API) authorize(method, path string) web.Handler { ac.EvalPermission(ac.ActionAlertingProvisioningRead), // organization scope ac.EvalPermission(ac.ActionAlertingNotificationsProvisioningRead), // organization scope ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets), // organization scope - } - if api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { - perms = append(perms, - ac.EvalPermission(ac.ActionAlertingReceiversRead), - ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), - ) + + ac.EvalPermission(ac.ActionAlertingReceiversRead), // organization scope + ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), // organization scope } eval = ac.EvalAny(perms...) diff --git a/pkg/services/ngalert/api/forking_alertmanager.go b/pkg/services/ngalert/api/forking_alertmanager.go index 39be2570e4f..df5059b296c 100644 --- a/pkg/services/ngalert/api/forking_alertmanager.go +++ b/pkg/services/ngalert/api/forking_alertmanager.go @@ -175,13 +175,6 @@ func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *contextmodel return f.GrafanaSvc.RouteGetSilences(ctx) } -func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableUserConfig) response.Response { - if !conf.AlertmanagerConfig.ReceiverType().Can(apimodels.GrafanaReceiverType) { - return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, conf.AlertmanagerConfig.ReceiverType().String())) - } - return f.GrafanaSvc.RoutePostAlertingConfig(ctx, conf) -} - func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response { return f.GrafanaSvc.RouteGetReceivers(ctx) } diff --git a/pkg/services/ngalert/api/generated_base_api_alertmanager.go b/pkg/services/ngalert/api/generated_base_api_alertmanager.go index 79ece30cdb9..68c3ccbedbf 100644 --- a/pkg/services/ngalert/api/generated_base_api_alertmanager.go +++ b/pkg/services/ngalert/api/generated_base_api_alertmanager.go @@ -42,7 +42,6 @@ type AlertmanagerApi interface { RouteGetSilences(*contextmodel.ReqContext) response.Response RoutePostAMAlerts(*contextmodel.ReqContext) response.Response RoutePostAlertingConfig(*contextmodel.ReqContext) response.Response - RoutePostGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response RoutePostGrafanaAlertingConfigHistoryActivate(*contextmodel.ReqContext) response.Response RoutePostTestGrafanaReceivers(*contextmodel.ReqContext) response.Response RoutePostTestGrafanaTemplates(*contextmodel.ReqContext) response.Response @@ -162,14 +161,6 @@ func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *contextmodel.ReqCo } return f.handleRoutePostAlertingConfig(ctx, conf, datasourceUIDParam) } -func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response { - // Parse Request Body - conf := apimodels.PostableUserConfig{} - if err := web.Bind(ctx.Req, &conf); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) - } - return f.handleRoutePostGrafanaAlertingConfig(ctx, conf) -} func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfigHistoryActivate(ctx *contextmodel.ReqContext) response.Response { // Parse Path Parameters idParam := web.Params(ctx.Req)[":id"] @@ -458,18 +449,6 @@ func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics m, ), ) - group.Post( - toMacaronPath("/api/alertmanager/grafana/config/api/v1/alerts"), - requestmeta.SetOwner(requestmeta.TeamAlerting), - requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), - api.authorize(http.MethodPost, "/api/alertmanager/grafana/config/api/v1/alerts"), - metrics.Instrument( - http.MethodPost, - "/api/alertmanager/grafana/config/api/v1/alerts", - api.Hooks.Wrap(srv.RoutePostGrafanaAlertingConfig), - m, - ), - ) group.Post( toMacaronPath("/api/alertmanager/grafana/config/history/{id}/_activate"), requestmeta.SetOwner(requestmeta.TeamAlerting), diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index e434d4c92fc..9e922024312 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -16,17 +16,6 @@ import ( alertingmodels "github.com/grafana/alerting/models" ) -// swagger:route POST /alertmanager/grafana/config/api/v1/alerts alertmanager RoutePostGrafanaAlertingConfig -// -// sets an Alerting config -// -// This API is designated to internal use only and can be removed or changed at any time without prior notice. -// -// Deprecated: true -// Responses: -// 201: Ack -// 400: ValidationError - // swagger:route POST /alertmanager/{DatasourceUID}/config/api/v1/alerts alertmanager RoutePostAlertingConfig // // sets an Alerting config diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go index 72ca0ebb730..0e8c319113b 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go @@ -2,12 +2,10 @@ package definitions import ( "fmt" - tmplhtml "html/template" "regexp" "strings" - tmpltext "text/template" - "github.com/prometheus/alertmanager/template" + "github.com/grafana/alerting/templates" "gopkg.in/yaml.v3" ) @@ -35,17 +33,12 @@ func (t *NotificationTemplate) Validate() error { t.Template = content // Validate template contents. We try to stick as close to what will actually happen when the templates are parsed - // by the alertmanager as possible. That means parsing with both the text and html parsers and making sure we set - // the template name and options. - ttext := tmpltext.New(t.Name).Option("missingkey=zero") - ttext.Funcs(tmpltext.FuncMap(template.DefaultFuncs)) - if _, err := ttext.Parse(t.Template); err != nil { - return fmt.Errorf("invalid template: %w", err) + // by the alertmanager as possible. + tmpl, err := templates.NewTemplate() + if err != nil { + return fmt.Errorf("failed to create template: %w", err) } - - thtml := tmplhtml.New(t.Name).Option("missingkey=zero") - thtml.Funcs(tmplhtml.FuncMap(template.DefaultFuncs)) - if _, err := thtml.Parse(t.Template); err != nil { + if err := tmpl.Parse(strings.NewReader(t.Template)); err != nil { return fmt.Errorf("invalid template: %w", err) } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go index 4fbbd983b13..74f186003d7 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation_test.go @@ -442,7 +442,6 @@ func TestValidateNotificationTemplates(t *testing.T) { expError: errors.New("invalid template: template: Different name than definition:1: template: multiple definition of template \"Alert Instance Template\""), }, { - // This is fine as long as the template name is different from the definition, it just ignores the extra text. name: "Extra text outside definition block - different template name and definition", template: NotificationTemplate{ Name: "Different name than definition", @@ -452,16 +451,16 @@ func TestValidateNotificationTemplates(t *testing.T) { expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, expError: nil, }, + // This test used to error because our template code parsed the template with the filename as template name. + // However, we have since moved away from this. We keep this test to ensure we don't regress. { - // This is NOT fine as the template name is the same as the definition. - // GO template parser will treat it as if it's wrapped in {{ define "Alert Instance Template" }}, thus creating a duplicate definition. name: "Extra text outside definition block - same template name and definition", template: NotificationTemplate{ Name: "Alert Instance Template", Template: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, Provenance: "test", }, - expError: errors.New("invalid template: template: Alert Instance Template:1: template: multiple definition of template \"Alert Instance Template\""), + expContent: `{{ define "Alert Instance Template" }}\nFiring: {{ .Labels.alertname }}\nSilence: {{ .SilenceURL }}\n{{ end }}[what is this?]`, }, } diff --git a/pkg/services/ngalert/api/tooling/definitions/contact_points.go b/pkg/services/ngalert/api/tooling/definitions/contact_points.go index 597694e6d60..9acbe144e88 100644 --- a/pkg/services/ngalert/api/tooling/definitions/contact_points.go +++ b/pkg/services/ngalert/api/tooling/definitions/contact_points.go @@ -313,16 +313,24 @@ type WebhookIntegration struct { URL string `json:"url" yaml:"url" hcl:"url"` - HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"` - MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"` - AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"` - AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"` - User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"` - Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"` - Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` - Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` - TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tlsConfig,block"` - HMACConfig *HMACConfig `json:"hmacConfig,omitempty" yaml:"hmacConfig,omitempty" hcl:"hmacConfig,block"` + HTTPMethod *string `json:"httpMethod,omitempty" yaml:"httpMethod,omitempty" hcl:"http_method"` + MaxAlerts *int64 `json:"maxAlerts,omitempty" yaml:"maxAlerts,omitempty" hcl:"max_alerts"` + AuthorizationScheme *string `json:"authorization_scheme,omitempty" yaml:"authorization_scheme,omitempty" hcl:"authorization_scheme"` + AuthorizationCredentials *Secret `json:"authorization_credentials,omitempty" yaml:"authorization_credentials,omitempty" hcl:"authorization_credentials"` + User *string `json:"username,omitempty" yaml:"username,omitempty" hcl:"basic_auth_user"` + Password *Secret `json:"password,omitempty" yaml:"password,omitempty" hcl:"basic_auth_password"` + ExtraHeaders *map[string]string `json:"headers,omitempty" yaml:"headers,omitempty" hcl:"headers"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + TLSConfig *TLSConfig `json:"tlsConfig,omitempty" yaml:"tlsConfig,omitempty" hcl:"tlsConfig,block"` + HMACConfig *HMACConfig `json:"hmacConfig,omitempty" yaml:"hmacConfig,omitempty" hcl:"hmacConfig,block"` + + Payload *CustomPayload `json:"payload,omitempty" yaml:"payload,omitempty" hcl:"payload,block"` +} + +type CustomPayload struct { + Template *string `json:"template,omitempty" yaml:"template,omitempty" hcl:"template"` + Vars *map[string]string `json:"vars,omitempty" yaml:"vars,omitempty" hcl:"vars"` } type HMACConfig struct { diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index e69d4d7550c..a8c9b24f49e 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -5696,38 +5696,6 @@ "tags": [ "alertmanager" ] - }, - "post": { - "deprecated": true, - "description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.", - "operationId": "RoutePostGrafanaAlertingConfig", - "parameters": [ - { - "in": "body", - "name": "Body", - "schema": { - "$ref": "#/definitions/PostableUserConfig" - } - } - ], - "responses": { - "201": { - "description": "Ack", - "schema": { - "$ref": "#/definitions/Ack" - } - }, - "400": { - "description": "ValidationError", - "schema": { - "$ref": "#/definitions/ValidationError" - } - } - }, - "summary": "sets an Alerting config", - "tags": [ - "alertmanager" - ] } }, "/alertmanager/grafana/config/api/v1/receivers": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index c341b12ab4c..1875f799b8b 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -321,38 +321,6 @@ } } }, - "post": { - "description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.", - "tags": [ - "alertmanager" - ], - "summary": "sets an Alerting config", - "operationId": "RoutePostGrafanaAlertingConfig", - "deprecated": true, - "parameters": [ - { - "name": "Body", - "in": "body", - "schema": { - "$ref": "#/definitions/PostableUserConfig" - } - } - ], - "responses": { - "201": { - "description": "Ack", - "schema": { - "$ref": "#/definitions/Ack" - } - }, - "400": { - "description": "ValidationError", - "schema": { - "$ref": "#/definitions/ValidationError" - } - } - } - }, "delete": { "description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.", "tags": [ diff --git a/pkg/services/ngalert/notifier/channels_config/available_channels.go b/pkg/services/ngalert/notifier/channels_config/available_channels.go index 6d3429a7359..5b70d7f58fa 100644 --- a/pkg/services/ngalert/notifier/channels_config/available_channels.go +++ b/pkg/services/ngalert/notifier/channels_config/available_channels.go @@ -952,6 +952,13 @@ func GetAvailableNotifiers() []*NotifierPlugin { PropertyName: "authorization_credentials", Secure: true, }, + { // New in 12.0. + Label: "Extra Headers", + Description: "Optionally provide extra headers to be used in the request.", + Element: ElementTypeKeyValueMap, + InputType: InputTypeText, + PropertyName: "headers", + }, { // New in 8.0. TODO: How to enforce only numbers? Label: "Max Alerts", Description: "Max alerts to include in a notification. Remaining alerts in the same batch will be ignored above this number. 0 means no limit.", @@ -974,6 +981,30 @@ func GetAvailableNotifiers() []*NotifierPlugin { PropertyName: "message", Placeholder: alertingTemplates.DefaultMessageEmbed, }, + { // New in 12.0. + Label: "Custom Payload", + Description: "Optionally provide a templated payload. Overrides 'Message' and 'Title' field.", + Element: ElementTypeSubform, + PropertyName: "payload", + SubformOptions: []NotifierOption{ + { + Label: "Payload Template", + Description: "Custom payload template.", + Element: ElementTypeTextArea, + PropertyName: "template", + Placeholder: `{{ template "webhook.default.payload" . }}`, + Required: true, + }, + { + Label: "Payload Variables", + Description: "Optionally provide a variables to be used in the payload template. They will be available in the template as `.Vars.`.", + Element: ElementTypeKeyValueMap, + InputType: InputTypeText, + PropertyName: "vars", + }, + }, + }, + { Label: "TLS", PropertyName: "tlsConfig", diff --git a/pkg/services/ngalert/notifier/templates.go b/pkg/services/ngalert/notifier/templates.go index 9f9a49f722c..5611bbe2b4c 100644 --- a/pkg/services/ngalert/notifier/templates.go +++ b/pkg/services/ngalert/notifier/templates.go @@ -15,8 +15,8 @@ type TestTemplatesResults = alertingNotify.TestTemplatesResults var ( DefaultLabels = map[string]string{ - prometheusModel.AlertNameLabel: `alert title`, - alertingModels.FolderTitleLabel: `folder title`, + prometheusModel.AlertNameLabel: `TestAlert`, + alertingModels.FolderTitleLabel: `Test Folder`, } DefaultAnnotations = map[string]string{ alertingModels.ValuesAnnotation: `{"B":22,"C":1}`, diff --git a/pkg/services/ngalert/notifier/templates_test.go b/pkg/services/ngalert/notifier/templates_test.go index 902c534a058..1b1da5a2890 100644 --- a/pkg/services/ngalert/notifier/templates_test.go +++ b/pkg/services/ngalert/notifier/templates_test.go @@ -84,7 +84,7 @@ CommonAnnotations: {{ range .CommonAnnotations.SortedPairs }}{{ .Name }}={{ .Val expected: TestTemplatesResults{ Results: []alertingNotify.TestTemplatesResult{{ Name: "slack.title", - Text: "\nReceiver: TestReceiver\nStatus: firing\nExternalURL: http://localhost:9093\nAlerts: 1\nFiring Alerts: 1\nResolved Alerts: 0\nGroupLabels: group_label=group_label_value \nCommonLabels: alertname=alert1 grafana_folder=folder title lbl1=val1 \nCommonAnnotations: ann1=annv1 \n", + Text: "\nReceiver: TestReceiver\nStatus: firing\nExternalURL: http://localhost:9093\nAlerts: 1\nFiring Alerts: 1\nResolved Alerts: 0\nGroupLabels: group_label=group_label_value \nCommonLabels: alertname=alert1 grafana_folder=Test Folder lbl1=val1 \nCommonAnnotations: ann1=annv1 \n", Scope: alertingNotify.TemplateScope(apimodels.RootScope), }}, Errors: nil, diff --git a/pkg/services/ngalert/notifier/testreceivers.go b/pkg/services/ngalert/notifier/testreceivers.go index 5844ff3aa30..cbd1ca29f87 100644 --- a/pkg/services/ngalert/notifier/testreceivers.go +++ b/pkg/services/ngalert/notifier/testreceivers.go @@ -5,6 +5,7 @@ import ( "encoding/json" alertingNotify "github.com/grafana/alerting/notify" + v2 "github.com/prometheus/alertmanager/api/v2" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) @@ -30,13 +31,17 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei }, }) } - var alert *alertingNotify.TestReceiversConfigAlertParams + a := &alertingNotify.PostableAlert{} if c.Alert != nil { - alert = &alertingNotify.TestReceiversConfigAlertParams{Annotations: c.Alert.Annotations, Labels: c.Alert.Labels} + a.Annotations = v2.ModelLabelSetToAPILabelSet(c.Alert.Annotations) + a.Labels = v2.ModelLabelSetToAPILabelSet(c.Alert.Labels) } - + AddDefaultLabelsAndAnnotations(a) return am.Base.TestReceivers(ctx, alertingNotify.TestReceiversConfigBodyParams{ - Alert: alert, + Alert: &alertingNotify.TestReceiversConfigAlertParams{ + Annotations: v2.APILabelSetToModelLabelSet(a.Annotations), + Labels: v2.APILabelSetToModelLabelSet(a.Labels), + }, Receivers: receivers, }) } diff --git a/pkg/services/pluginsintegration/pluginconfig/request_test.go b/pkg/services/pluginsintegration/pluginconfig/request_test.go index f8aa0cbc9f2..8153c24f10a 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/request_test.go @@ -311,7 +311,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { UsernameAssertion: true, }, UserIdentityFallbackCredentialsEnabled: true, - ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"}, + ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql", "grafana-azureprometheus-datasource"}, AzureEntraPasswordCredentialsEnabled: true, } diff --git a/pkg/services/rendering/capabilities_test.go b/pkg/services/rendering/capabilities_test.go index e9b4989dbbf..956fae30e0f 100644 --- a/pkg/services/rendering/capabilities_test.go +++ b/pkg/services/rendering/capabilities_test.go @@ -121,7 +121,7 @@ func TestCapabilities(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rs.Cfg.RendererUrl = tt.rendererUrl + rs.Cfg.RendererServerUrl = tt.rendererUrl rs.version = tt.rendererVersion res, err := rs.HasCapability(context.Background(), tt.capabilityName) diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index 0f877867465..705f9672240 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -71,7 +71,7 @@ func (rs *RenderingService) renderCSVViaHTTP(ctx context.Context, renderKey stri } func (rs *RenderingService) generateImageRendererURL(renderType RenderType, opts Opts, renderKey string) (*url.URL, error) { - rendererUrl := rs.Cfg.RendererUrl + rendererUrl := rs.Cfg.RendererServerUrl if renderType == RenderCSV { rendererUrl += "/csv" } @@ -242,7 +242,7 @@ func (rs *RenderingService) getRemotePluginVersionWithRetry(callback func(string } func (rs *RenderingService) getRemotePluginVersion() (string, error) { - rendererURL, err := url.Parse(rs.Cfg.RendererUrl + "/version") + rendererURL, err := url.Parse(rs.Cfg.RendererServerUrl + "/version") if err != nil { return "", err } diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 8f25cb8da22..2585f9b783e 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -27,18 +27,19 @@ import ( var _ Service = (*RenderingService)(nil) type RenderingService struct { - log log.Logger - plugin Plugin - renderAction renderFunc - renderCSVAction renderCSVFunc - sanitizeSVGAction sanitizeFunc - sanitizeURL string - domain string - inProgressCount int32 - version string - versionMutex sync.RWMutex - capabilities []Capability - pluginAvailable bool + log log.Logger + plugin Plugin + renderAction renderFunc + renderCSVAction renderCSVFunc + sanitizeSVGAction sanitizeFunc + sanitizeURL string + domain string + inProgressCount int32 + version string + versionMutex sync.RWMutex + capabilities []Capability + pluginAvailable bool + rendererCallbackURL string perRequestRenderKeyProvider renderKeyProvider Cfg *setting.Cfg @@ -80,9 +81,24 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, remot // value used for domain attribute of renderKey cookie var domain string + // value used by the image renderer to make requests to Grafana + rendererCallbackURL := cfg.RendererCallbackUrl + if cfg.RendererServerUrl != "" { + sanitizeURL = getSanitizerURL(cfg.RendererServerUrl) + + // Default value for callback URL using a remote renderer should be AppURL + if rendererCallbackURL == "" { + rendererCallbackURL = cfg.AppURL + } + } + switch { - case cfg.RendererCallbackUrl != "": - u, err := url.Parse(cfg.RendererCallbackUrl) + case rendererCallbackURL != "": + if rendererCallbackURL[len(rendererCallbackURL)-1] != '/' { + rendererCallbackURL += "/" + } + + u, err := url.Parse(rendererCallbackURL) if err != nil { logger.Warn("Image renderer callback url is not valid. " + "Please provide a valid RendererCallbackUrl. " + @@ -96,10 +112,6 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, remot domain = "localhost" } - if cfg.RendererUrl != "" { - sanitizeURL = getSanitizerURL(cfg.RendererUrl) - } - var renderKeyProvider renderKeyProvider if features.IsEnabledGlobally(featuremgmt.FlagRenderAuthJWT) { renderKeyProvider = &jwtRenderKeyProvider{ @@ -145,6 +157,7 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, remot domain: domain, sanitizeURL: sanitizeURL, pluginAvailable: exists, + rendererCallbackURL: rendererCallbackURL, } gob.Register(&RenderUser{}) @@ -215,7 +228,7 @@ func (rs *RenderingService) Run(ctx context.Context) error { } func (rs *RenderingService) remoteAvailable() bool { - return rs.Cfg.RendererUrl != "" + return rs.Cfg.RendererServerUrl != "" } func (rs *RenderingService) IsAvailable(ctx context.Context) bool { @@ -424,13 +437,14 @@ func (rs *RenderingService) getNewFilePath(rt RenderType) (string, error) { // getGrafanaCallbackURL creates a URL to send to the image rendering as callback for rendering a Grafana resource func (rs *RenderingService) getGrafanaCallbackURL(path string) string { - if rs.Cfg.RendererUrl != "" || rs.Cfg.RendererCallbackUrl != "" { - // The backend rendering service can potentially be remote. - // So we need to use the root_url to ensure the rendering service - // can reach this Grafana instance. + if rs.rendererCallbackURL != "" { + // rendererCallbackURL should be set if: + // - the backend rendering service is remote (default value is cfg.AppURL + // and set when initializing the service) + // - the service is a plugin and Grafana is running behind a proxy changing its domain // &render=1 signals to the legacy redirect layer to - return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path) + return fmt.Sprintf("%s%s&render=1", rs.rendererCallbackURL, path) } protocol := rs.Cfg.Protocol diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index 2bcd6463633..15f473b555c 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" ) @@ -25,21 +26,21 @@ func TestGetUrl(t *testing.T) { } t.Run("When renderer and callback url configured should return callback url plus path", func(t *testing.T) { - rs.Cfg.RendererUrl = "http://localhost:8081/render" - rs.Cfg.RendererCallbackUrl = "http://public-grafana.com/" + rs.Cfg.RendererServerUrl = "http://localhost:8081/render" + rs.rendererCallbackURL = "http://public-grafana.com/" url := rs.getGrafanaCallbackURL(path) - require.Equal(t, rs.Cfg.RendererCallbackUrl+path+"&render=1", url) + require.Equal(t, rs.rendererCallbackURL+path+"&render=1", url) }) t.Run("When callback url is configured and https should return domain of callback url plus path", func(t *testing.T) { - rs.Cfg.RendererCallbackUrl = "https://public-grafana.com/" + rs.rendererCallbackURL = "https://public-grafana.com/" url := rs.getGrafanaCallbackURL(path) - require.Equal(t, rs.Cfg.RendererCallbackUrl+path+"&render=1", url) + require.Equal(t, rs.rendererCallbackURL+path+"&render=1", url) }) t.Run("When renderer url not configured", func(t *testing.T) { - rs.Cfg.RendererUrl = "" - rs.Cfg.RendererCallbackUrl = "" + rs.Cfg.RendererServerUrl = "" + rs.rendererCallbackURL = "" rs.domain = "localhost" rs.Cfg.HTTPPort = "3000" @@ -128,8 +129,8 @@ func TestRenderLimitImage(t *testing.T) { rs := RenderingService{ Cfg: &setting.Cfg{ - HomePath: path, - RendererUrl: "http://localhost:8081/render", + HomePath: path, + RendererServerUrl: "http://localhost:8081/render", }, inProgressCount: 2, log: log.New("test"), @@ -170,7 +171,7 @@ func TestRenderLimitImage(t *testing.T) { func TestRenderLimitImageError(t *testing.T) { rs := RenderingService{ Cfg: &setting.Cfg{ - RendererUrl: "http://localhost:8081/render", + RendererServerUrl: "http://localhost:8081/render", }, inProgressCount: 2, log: log.New("test"), @@ -201,7 +202,7 @@ func TestRenderingServiceGetRemotePluginVersion(t *testing.T) { })) defer server.Close() - rs.Cfg.RendererUrl = server.URL + "/render" + rs.Cfg.RendererServerUrl = server.URL + "/render" version, err := rs.getRemotePluginVersion() require.NoError(t, err) @@ -214,7 +215,7 @@ func TestRenderingServiceGetRemotePluginVersion(t *testing.T) { })) defer server.Close() - rs.Cfg.RendererUrl = server.URL + "/render" + rs.Cfg.RendererServerUrl = server.URL + "/render" version, err := rs.getRemotePluginVersion() require.NoError(t, err) @@ -239,7 +240,7 @@ func TestRenderingServiceGetRemotePluginVersion(t *testing.T) { })) defer server.Close() - rs.Cfg.RendererUrl = server.URL + "/render" + rs.Cfg.RendererServerUrl = server.URL + "/render" remoteVersionFetchInterval = time.Millisecond remoteVersionFetchRetries = 5 go func() { @@ -249,3 +250,76 @@ func TestRenderingServiceGetRemotePluginVersion(t *testing.T) { require.Eventually(t, func() bool { return rs.Version() == "3.1.4159" }, time.Second, time.Millisecond) }) } + +func TestProvideService(t *testing.T) { + cfg := setting.NewCfg() + cfg.AppURL = "http://app-url" + cfg.ImagesDir = filepath.Join(t.TempDir(), "images") + cfg.CSVsDir = filepath.Join(t.TempDir(), "csvs") + cfg.PDFsDir = filepath.Join(t.TempDir(), "pdfs") + + t.Run("Default configuration values", func(t *testing.T) { + rs, err := ProvideService(cfg, featuremgmt.WithFeatures(), nil, &dummyPluginManager{}) + require.NoError(t, err) + + require.Equal(t, "", rs.Cfg.RendererServerUrl) + require.Equal(t, "", rs.rendererCallbackURL) + require.Equal(t, "", rs.domain) + }) + + t.Run("RendererURL is set but not RendererCallbackUrl", func(t *testing.T) { + cfg.RendererServerUrl = "http://custom-renderer:8081" + cfg.RendererCallbackUrl = "" + + rs, err := ProvideService(cfg, featuremgmt.WithFeatures(), nil, &dummyPluginManager{}) + require.NoError(t, err) + + require.Equal(t, "http://custom-renderer:8081", rs.Cfg.RendererServerUrl) + require.Equal(t, "http://app-url/", rs.rendererCallbackURL) + require.Equal(t, "app-url", rs.domain) + }) + + t.Run("RendererURL and RendererCallbackUrl are set", func(t *testing.T) { + cfg.RendererServerUrl = "http://custom-renderer:8081" + cfg.RendererCallbackUrl = "http://public-grafana.com/" + + rs, err := ProvideService(cfg, featuremgmt.WithFeatures(), nil, &dummyPluginManager{}) + require.NoError(t, err) + + require.Equal(t, "http://custom-renderer:8081", rs.Cfg.RendererServerUrl) + require.Equal(t, "http://public-grafana.com/", rs.rendererCallbackURL) + require.Equal(t, "public-grafana.com", rs.domain) + }) + + t.Run("RendererURL is not set but RendererCallbackUrl is set", func(t *testing.T) { + cfg.RendererServerUrl = "" + cfg.RendererCallbackUrl = "https://public-grafana.com/" + + rs, err := ProvideService(cfg, featuremgmt.WithFeatures(), nil, &dummyPluginManager{}) + require.NoError(t, err) + + require.Equal(t, "", rs.Cfg.RendererServerUrl) + require.Equal(t, "https://public-grafana.com/", rs.rendererCallbackURL) + require.Equal(t, "public-grafana.com", rs.domain) + }) + + t.Run("RendererCallbackURL is missing trailing slash", func(t *testing.T) { + cfg.RendererServerUrl = "" + cfg.RendererCallbackUrl = "https://public-grafana.com" + + rs, err := ProvideService(cfg, featuremgmt.WithFeatures(), nil, &dummyPluginManager{}) + require.NoError(t, err) + + require.Equal(t, "", rs.Cfg.RendererServerUrl) + require.Equal(t, "https://public-grafana.com/", rs.rendererCallbackURL) + require.Equal(t, "public-grafana.com", rs.domain) + }) + + t.Run("RendererCallbackURL is invalid", func(t *testing.T) { + cfg.RendererServerUrl = "" + cfg.RendererCallbackUrl = "http://public{grafana" + + _, err := ProvideService(cfg, featuremgmt.WithFeatures(), nil, &dummyPluginManager{}) + require.Error(t, err) + }) +} diff --git a/pkg/services/store/kind/dashboard/testdata/devdash-all-panels-info.json b/pkg/services/store/kind/dashboard/testdata/devdash-all-panels-info.json index 7d6e29aae5e..f97a4796ba6 100644 --- a/pkg/services/store/kind/dashboard/testdata/devdash-all-panels-info.json +++ b/pkg/services/store/kind/dashboard/testdata/devdash-all-panels-info.json @@ -228,7 +228,7 @@ ] } ], - "schemaVersion": 33, + "schemaVersion": 36, "linkCount": 2, "timeFrom": "now-6h", "timeTo": "now", diff --git a/pkg/services/store/testdata/public_testdata.golden.jsonc b/pkg/services/store/testdata/public_testdata.golden.jsonc index ea77fd3d487..d18191f2d39 100644 --- a/pkg/services/store/testdata/public_testdata.golden.jsonc +++ b/pkg/services/store/testdata/public_testdata.golden.jsonc @@ -1,5 +1,5 @@ // 🌟 This was machine generated. Do not edit. 🌟 -// +// // Frame[0] { // "type": "directory-listing", // "typeVersion": [ @@ -10,7 +10,7 @@ // "HasMore": false // } // } -// Name: +// Name: // Dimensions: 3 Fields by 3 Rows // +----------------------------+----------------------+---------------+ // | Name: name | Name: mediaType | Name: size | @@ -87,4 +87,4 @@ } } ] -} \ No newline at end of file +} diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index e93c35aee46..49db0c53734 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -25,13 +25,13 @@ type Service interface { } func UIDToIDHandler(teamService Service) func(ctx context.Context, orgID int64, resourceID string) (string, error) { - return func(ctx context.Context, orgID int64, resourceID string) (string, error) { - // if teamID is empty or is an integer, we assume it's a team id and we don't need to resolve it - _, err := strconv.ParseInt(resourceID, 10, 64) - if resourceID == "" || err == nil { - return resourceID, nil + return func(ctx context.Context, orgID int64, teamIDorUID string) (string, error) { + // if teamIDorUID is empty or is an integer, we assume it's a team ID, and we don't need to resolve it + _, err := strconv.ParseInt(teamIDorUID, 10, 64) + if teamIDorUID == "" || err == nil { + return teamIDorUID, nil } - team, err := teamService.GetTeamByID(ctx, &GetTeamByIDQuery{UID: resourceID, OrgID: orgID}) + team, err := teamService.GetTeamByID(ctx, &GetTeamByIDQuery{UID: teamIDorUID, OrgID: orgID}) if err != nil { return "", err } @@ -44,9 +44,9 @@ func MiddlewareTeamUIDResolver(teamService Service, paramName string) web.Handle handler := UIDToIDHandler(teamService) return func(c *contextmodel.ReqContext) { - // Get team id from request, fetch team and replace teamId with team id - teamID := web.Params(c.Req)[paramName] - id, err := handler(c.Req.Context(), c.OrgID, teamID) + // Get team id from request, fetch team and replace team UID with team ID + teamIDorUID := web.Params(c.Req)[paramName] + id, err := handler(c.Req.Context(), c.OrgID, teamIDorUID) if err == nil { gotParams := web.Params(c.Req) gotParams[paramName] = id diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index f113836f857..3caefb9562e 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -20,6 +20,12 @@ func NewFakeService() *FakeService { return &FakeService{} } +func NewFakeServiceWithTeamDTO(teamDTO *team.TeamDTO) *FakeService { + return &FakeService{ + ExpectedTeamDTO: teamDTO, + } +} + func (s *FakeService) CreateTeam(ctx context.Context, cmd *team.CreateTeamCommand) (team.Team, error) { return s.ExpectedTeam, s.ExpectedError } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c20b0468699..4a77bfdbf04 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -144,7 +144,7 @@ type Cfg struct { ImagesDir string CSVsDir string PDFsDir string - RendererUrl string + RendererServerUrl string RendererCallbackUrl string RendererAuthToken string RendererConcurrentRequestLimit int @@ -1168,9 +1168,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.readZanzanaSettings() - if err := cfg.readRenderingSettings(iniFile); err != nil { - return err - } + cfg.readRenderingSettings(iniFile) cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) @@ -1796,26 +1794,12 @@ func readServiceAccountSettings(iniFile *ini.File, cfg *Cfg) error { return nil } -func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error { +func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) { renderSec := iniFile.Section("rendering") - cfg.RendererUrl = valueAsString(renderSec, "server_url", "") + cfg.RendererServerUrl = valueAsString(renderSec, "server_url", "") cfg.RendererCallbackUrl = valueAsString(renderSec, "callback_url", "") cfg.RendererAuthToken = valueAsString(renderSec, "renderer_token", "-") - if cfg.RendererCallbackUrl == "" { - cfg.RendererCallbackUrl = AppUrl - } else { - if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' { - cfg.RendererCallbackUrl += "/" - } - _, err := url.Parse(cfg.RendererCallbackUrl) - if err != nil { - // XXX: Should return an error? - cfg.Logger.Error("Invalid callback_url.", "url", cfg.RendererCallbackUrl, "error", err) - os.Exit(1) - } - } - cfg.RendererConcurrentRequestLimit = renderSec.Key("concurrent_render_request_limit").MustInt(30) cfg.RendererRenderKeyLifeTime = renderSec.Key("render_key_lifetime").MustDuration(5 * time.Minute) cfg.RendererDefaultImageWidth = renderSec.Key("default_image_width").MustInt(1000) @@ -1824,8 +1808,6 @@ func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) error { cfg.ImagesDir = filepath.Join(cfg.DataPath, "png") cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv") cfg.PDFsDir = filepath.Join(cfg.DataPath, "pdf") - - return nil } func (cfg *Cfg) readAlertingSettings(iniFile *ini.File) error { diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index 212a5bfc181..80423e93631 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -29,9 +29,10 @@ func extractPluginSettings(sections []*ini.Section) PluginSettings { var ( defaultPreinstallPlugins = map[string]InstallPlugin{ // Default preinstalled plugins - "grafana-lokiexplore-app": {"grafana-lokiexplore-app", "", ""}, - "grafana-pyroscope-app": {"grafana-pyroscope-app", "", ""}, - "grafana-exploretraces-app": {"grafana-exploretraces-app", "", ""}, + "grafana-lokiexplore-app": {"grafana-lokiexplore-app", "", ""}, + "grafana-pyroscope-app": {"grafana-pyroscope-app", "", ""}, + "grafana-exploretraces-app": {"grafana-exploretraces-app", "", ""}, + "grafana-metricsdrilldown-app": {"grafana-metricsdrilldown-app", "", ""}, } ) @@ -59,9 +60,6 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { if cfg.IsFeatureToggleEnabled("grafanaAdvisor") { // Use literal string to avoid circular dependency preinstallPlugins["grafana-advisor-app"] = InstallPlugin{"grafana-advisor-app", "", ""} } - if cfg.IsFeatureToggleEnabled("exploreMetricsUseExternalAppPlugin") { // Use literal string to avoid circular dependency - preinstallPlugins["grafana-metricsdrilldown-app"] = InstallPlugin{"grafana-metricsdrilldown-app", "", ""} - } // Add the plugins defined in the configuration for _, plugin := range rawInstallPlugins { parts := strings.Split(plugin, "@") diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index b112bd3130a..0077bf27ef4 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -33,7 +33,7 @@ func TestLoadingSettings(t *testing.T) { require.Nil(t, err) require.Equal(t, "admin", cfg.AdminUser) - require.Equal(t, "http://localhost:3000/", cfg.RendererCallbackUrl) + require.Equal(t, "", cfg.RendererCallbackUrl) require.Equal(t, "TLS1.2", cfg.MinTLSVersion) }) @@ -255,17 +255,6 @@ func TestLoadingSettings(t *testing.T) { require.Equal(t, hostname, cfg.InstanceName) }) - t.Run("Reading callback_url should add trailing slash", func(t *testing.T) { - cfg := NewCfg() - err := cfg.Load(CommandLineArgs{ - HomePath: "../../", - Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"}, - }) - require.Nil(t, err) - - require.Equal(t, "http://myserver/renderer/", cfg.RendererCallbackUrl) - }) - t.Run("Only sync_ttl should return the value sync_ttl", func(t *testing.T) { cfg := NewCfg() err := cfg.Load(CommandLineArgs{ diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index b3cbb1b88c8..9aafd63534f 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -23,7 +23,7 @@ require ( github.com/stretchr/testify v1.10.0 gocloud.dev v0.40.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 - google.golang.org/grpc v1.71.0 + google.golang.org/grpc v1.71.1 k8s.io/apimachinery v0.32.3 k8s.io/apiserver v0.32.3 k8s.io/client-go v0.32.3 @@ -202,7 +202,7 @@ require ( github.com/googleapis/go-sql-spanner v1.11.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 // indirect + github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 // indirect github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect @@ -210,8 +210,9 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.35.1 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.275.0 // indirect github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect + github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250411131846-e7b32d622991 // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect @@ -320,7 +321,7 @@ require ( github.com/prometheus/alertmanager v0.27.0 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.13.2 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -365,10 +366,10 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect @@ -383,16 +384,16 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.15.1 // indirect google.golang.org/api v0.223.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index c7bcf3afc45..d91459f5b66 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -1249,8 +1249,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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692 h1:5mOChD6fbkxeafK1iuy/iO/qKLyHeougMTtRJSgvAUM= -github.com/grafana/alerting v0.0.0-20250408102153-2412c378a692/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430 h1:qT0D7AIV0GRu8JUrSJYuyzj86kqLgksKQjwD++DqyOM= +github.com/grafana/alerting v0.0.0-20250411135245-cad0d384d430/go.mod h1:3ER/8BhIEhvrddcztLQSc5ez1f1jNHIPdquc1F+DzOw= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d h1:TDVZemfYeJHPyXeYCnqL7BQqsa+mpaZYth/Qm3TKaT8= github.com/grafana/authlib v0.0.0-20250325095148-d6da9c164a7d/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us= github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM= @@ -1269,13 +1269,14 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= +github.com/grafana/grafana-plugin-sdk-go v0.275.0 h1:icGmZG91lVqIo79w/pSki6N44d3IjOjTfsfQPfu4THU= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 h1:wdJy5x6M7auWDjUIubqhfZuZvphUMyjD7hxB3RqV4aE= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043/go.mod h1:jwYig4wlnLLq4HQKDpS95nDeZi4+DmcD17KYYS1gMJg= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= -github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9 h1:kJWonBYechx35NbOUVf1ulufKyjH1UlDJDXlk8bdGn0= +github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250411131846-e7b32d622991 h1:3gcAHB3K1tSJ3jPY7EYm4G/TBimwcETyZVFQDsx18iA= +github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250411131846-e7b32d622991/go.mod h1:Lmze5IWgV2qTGY0l1JQGRXpWoNGfeGX/HXdc8ByMfE4= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 h1:wSqgLKFwI7fyeqf3djRXGClBLb/UPjZ4XPm/UsKFDB0= @@ -1651,8 +1652,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ= @@ -1847,14 +1848,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= @@ -1917,8 +1918,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2053,8 +2054,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2086,8 +2087,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2106,8 +2107,8 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2210,8 +2211,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -2224,8 +2225,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2244,16 +2245,16 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -2326,8 +2327,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2603,8 +2604,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index c447e0277ad..557b56bc9b1 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -14,7 +14,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana-app-sdk/logging v0.35.1 - github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 + github.com/grafana/grafana-plugin-sdk-go v0.275.0 github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250401081501-6af5fbf3fff0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 @@ -25,13 +25,16 @@ require ( go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 gocloud.dev v0.40.0 - golang.org/x/sync v0.12.0 - google.golang.org/grpc v1.71.0 + golang.org/x/sync v0.13.0 + google.golang.org/grpc v1.71.1 google.golang.org/protobuf v1.36.6 k8s.io/apimachinery v0.32.3 ) -require github.com/go-jose/go-jose/v3 v3.0.4 +require ( + github.com/go-jose/go-jose/v3 v3.0.4 + github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250411131846-e7b32d622991 +) require ( cel.dev/expr v0.19.1 // indirect @@ -154,7 +157,7 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -170,10 +173,10 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect @@ -182,15 +185,15 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.223.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 946f97fab8e..da9c1dc396d 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -269,10 +269,12 @@ github.com/grafana/grafana-app-sdk v0.35.1 h1:zEXubzsQrxGBOzXJJMBwhEClC/tvPi0sfK github.com/grafana/grafana-app-sdk v0.35.1/go.mod h1:Zx5MkVppYK+ElSDUAR6+fjzOVo6I/cIgk+ty+LmNOxI= github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmwA9wvdwdzJTZsv9buM= github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 h1:qVdhLR+XkVdTQ2Sr7+VnRfGM8RMp8oPe25nghsSpQms= -github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= +github.com/grafana/grafana-plugin-sdk-go v0.275.0 h1:icGmZG91lVqIo79w/pSki6N44d3IjOjTfsfQPfu4THU= +github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 h1:wdJy5x6M7auWDjUIubqhfZuZvphUMyjD7hxB3RqV4aE= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043/go.mod h1:jwYig4wlnLLq4HQKDpS95nDeZi4+DmcD17KYYS1gMJg= +github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250411131846-e7b32d622991 h1:3gcAHB3K1tSJ3jPY7EYm4G/TBimwcETyZVFQDsx18iA= +github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250411131846-e7b32d622991/go.mod h1:Lmze5IWgV2qTGY0l1JQGRXpWoNGfeGX/HXdc8ByMfE4= 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/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -393,8 +395,8 @@ github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuF github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= @@ -462,14 +464,14 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0 h1:D3htJISCUU/wOVlKwisVKancWm+2U4h9xDEaiMkiyRE= -go.opentelemetry.io/contrib/propagators/jaeger v1.34.0/go.mod h1:DAX1bsj+uDm2ZuOQH/RgZRx7RQZWyzV5W2WR/0UX8JA= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0 h1:UIrZgRBHUrYRlJ4V419lVb4rs2ar0wFzKNAebaP05XU= +go.opentelemetry.io/contrib/propagators/jaeger v1.35.0/go.mod h1:0ciyFyYZxE6JqRAQvIgGRabKWDUmNdW3GAQb6y/RlFU= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0 h1:VpYbyLrB5BS3blBCJMqHRIrbU4RlPnyFovR3La+1j4Q= go.opentelemetry.io/contrib/samplers/jaegerremote v0.29.0/go.mod h1:XAJmM2MWhiIoTO4LCLBVeE8w009TmsYk6hq1UNdXs5A= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= @@ -508,8 +510,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -550,11 +552,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -564,8 +566,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -590,8 +592,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -607,10 +609,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -626,8 +628,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -661,8 +663,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index a98d5d0cfc1..f6fa4e02475 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -563,7 +563,7 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size return err } } - return err + return iter.Error() }) return rv, err }) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 81fc94ae146..816ef58791b 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -18,6 +18,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -32,15 +33,18 @@ type ResourceServer interface { } type ListIterator interface { + // Next advances iterator and returns true if there is next value is available from the iterator. + // Error() should be checked after every call of Next(), even when Next() returns true. Next() bool // sql.Rows - // Iterator error (if exts) + // Error returns iterator error, if any. This should be checked after any Next() call. + // (Some iterator implementations return true from Next, but also set the error at the same time). Error() error - // The token that can be used to start iterating *after* this item + // ContinueToken returns the token that can be used to start iterating *after* this item ContinueToken() string - // The token that can be used to start iterating *before* this item + // ContinueTokenWithCurrentRV returns the token that can be used to start iterating *before* this item ContinueTokenWithCurrentRV() string // ResourceVersion of the current item @@ -380,23 +384,16 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour return nil, NewBadRequestError("can not save annotation: " + utils.AnnoKeyGrantPermissions) } - check := claims.CheckRequest{ - Verb: utils.VerbCreate, - Group: key.Group, - Resource: key.Resource, - Namespace: key.Namespace, - } - event := &WriteEvent{ Value: value, Key: key, Object: obj, } + if oldValue == nil { event.Type = WatchEvent_ADDED } else { event.Type = WatchEvent_MODIFIED - check.Verb = utils.VerbUpdate temp := &unstructured.Unstructured{} err = temp.UnmarshalJSON(oldValue) @@ -440,19 +437,34 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour return nil, err } - // We only set name for update checks - if check.Verb == utils.VerbUpdate { - check.Name = key.Name - } + // For folder moves, we need to check permissions on both folders + if s.isFolderMove(event) { + if err := s.checkFolderMovePermissions(ctx, user, key, event.ObjectOld.GetFolder(), obj.GetFolder()); err != nil { + return nil, err + } + } else { + // Regular permission check for create/update + check := claims.CheckRequest{ + Verb: utils.VerbCreate, + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + } - check.Folder = obj.GetFolder() - a, err := s.access.Check(ctx, user, check) - if err != nil { - return nil, AsErrorResult(err) - } - if !a.Allowed { - return nil, &ErrorResult{ - Code: http.StatusForbidden, + if event.Type == WatchEvent_MODIFIED { + check.Verb = utils.VerbUpdate + check.Name = key.Name + } + + check.Folder = obj.GetFolder() + a, err := s.access.Check(ctx, user, check) + if err != nil { + return nil, AsErrorResult(err) + } + if !a.Allowed { + return nil, &ErrorResult{ + Code: http.StatusForbidden, + } } } @@ -466,6 +478,59 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour return event, nil } +// isFolderMove determines if an event represents a resource being moved between folders +func (s *server) isFolderMove(event *WriteEvent) bool { + return event.Type == WatchEvent_MODIFIED && + event.ObjectOld != nil && + event.ObjectOld.GetFolder() != event.Object.GetFolder() +} + +// checkFolderMovePermissions handles permission checks when a resource is being moved between folders +func (s *server) checkFolderMovePermissions(ctx context.Context, user claims.AuthInfo, key *ResourceKey, oldFolder, newFolder string) *ErrorResult { + // First check if user can update the resource in the original folder + updateCheck := claims.CheckRequest{ + Verb: utils.VerbUpdate, + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + Name: key.Name, + Folder: oldFolder, + } + + a, err := s.access.Check(ctx, user, updateCheck) + if err != nil { + return AsErrorResult(err) + } + if !a.Allowed { + return &ErrorResult{ + Code: http.StatusForbidden, + Message: "not allowed to update resource in the source folder", + } + } + + // Then check if user can create the resource in the destination folder + createCheck := claims.CheckRequest{ + Verb: utils.VerbCreate, + Group: key.Group, + Resource: key.Resource, + Namespace: key.Namespace, + Folder: newFolder, + } + + a, err = s.access.Check(ctx, user, createCheck) + if err != nil { + return AsErrorResult(err) + } + if !a.Allowed { + return &ErrorResult{ + Code: http.StatusForbidden, + Message: "not allowed to create resource in the destination folder", + } + } + + return nil +} + func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateResponse, error) { ctx, span := s.tracer.Start(ctx, "storage_server.Create") defer span.End() @@ -763,11 +828,10 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err if iter.Next() { rsp.NextPageToken = t } - - break + return iter.Error() } } - return nil + return iter.Error() }) if err != nil { rsp.Error = AsErrorResult(err) @@ -871,7 +935,7 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { return err } } - return nil + return iter.Error() }) if err != nil { return err diff --git a/pkg/storage/unified/sql/notifier_sql_test.go b/pkg/storage/unified/sql/notifier_sql_test.go index 198e3f4da18..b2c8630f264 100644 --- a/pkg/storage/unified/sql/notifier_sql_test.go +++ b/pkg/storage/unified/sql/notifier_sql_test.go @@ -2,13 +2,14 @@ package sql import ( "context" + "sync" "testing" "time" + "github.com/grafana/grafana-app-sdk/logging" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" - "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) @@ -231,11 +232,16 @@ func TestPollingNotifier(t *testing.T) { } var historyPollCalled bool + once := sync.Once{} historyPoll := func(ctx context.Context, grp string, res string, since int64) ([]*historyPollResponse, error) { - historyPollCalled = true - require.Equal(t, "test-group", grp) - require.Equal(t, "test-resource", res) - require.Equal(t, int64(0), since) + // only assert the first time - this may be called multiple times + // depending on the host hardware etc, due to timing issues... + once.Do(func() { + historyPollCalled = true + require.Equal(t, "test-group", grp) + require.Equal(t, "test-resource", res) + require.Equal(t, int64(0), since) + }) return []*historyPollResponse{testEvent}, nil } diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 8afaf50bacd..deb31b10fd9 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -29,6 +29,28 @@ func TestMain(m *testing.M) { testsuite.Run(m) } +func TestIntegrationStorageServer(t *testing.T) { + if infraDB.IsTestDBSpanner() { + t.Skip("skipping integration test") + } + unitest.RunStorageServerTest(t, func(ctx context.Context) resource.StorageBackend { + dbstore := infraDB.InitTestDB(t) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(t, err) + require.NotNil(t, eDB) + + backend, err := sql.NewBackend(sql.BackendOptions{ + DBProvider: eDB, + IsHA: true, + }) + require.NoError(t, err) + require.NotNil(t, backend) + err = backend.Init(testutil.NewDefaultTestContext(t)) + require.NoError(t, err) + return backend + }) +} + // TestStorageBackend is a test for the StorageBackend interface. func TestIntegrationSQLStorageBackend(t *testing.T) { if infraDB.IsTestDBSpanner() { diff --git a/pkg/storage/unified/testing/server.go b/pkg/storage/unified/testing/server.go new file mode 100644 index 00000000000..3195ef82637 --- /dev/null +++ b/pkg/storage/unified/testing/server.go @@ -0,0 +1,298 @@ +package test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/require" +) + +// RunStorageServerTest runs the storage server test suite +func RunStorageServerTest(t *testing.T, newBackend NewBackendFunc) { + runTestResourcePermissionScenarios(t, newBackend(context.Background()), GenerateRandomNSPrefix()) +} + +// func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend, nsPrefix string) { +func runTestResourcePermissionScenarios(t *testing.T, backend resource.StorageBackend, nsPrefix string) { + // Test user + testUser := &identity.StaticRequester{ + Type: types.TypeUser, + Login: "testuser", + UserID: 123, + UserUID: "u123", + OrgRole: identity.RoleAdmin, + IsGrafanaAdmin: true, + } + + testCases := []struct { + name string + initialFolder string + targetFolder string + permissionMap map[string]bool + expectSuccess bool + expectedChecks int + expectedError int32 // HTTP status code for error, 0 if no error expected + }{ + { + name: "Create resource in folder", + initialFolder: "folder1", + targetFolder: "", // No move, just create + permissionMap: map[string]bool{"folder1:create": true}, + expectSuccess: true, + expectedChecks: 1, + expectedError: 0, + }, + { + name: "Create resource denied", + initialFolder: "folder1", + targetFolder: "", // No move, just create + permissionMap: map[string]bool{"folder1:create": false}, + expectSuccess: false, + expectedChecks: 1, + expectedError: http.StatusForbidden, + }, + { + name: "Update resource in same folder", + initialFolder: "folder1", + targetFolder: "folder1", // Same folder, just update + permissionMap: map[string]bool{"folder1:update": true}, + expectSuccess: true, + expectedChecks: 1, + expectedError: 0, + }, + { + name: "Update resource denied", + initialFolder: "folder1", + targetFolder: "folder1", // Same folder + permissionMap: map[string]bool{"folder1:update": false}, + expectSuccess: false, + expectedChecks: 1, + expectedError: http.StatusForbidden, + }, + { + name: "Move resource to another folder - allowed", + initialFolder: "folder1", + targetFolder: "folder2", // Moving to a different folder + permissionMap: map[string]bool{"folder1:update": true, "folder2:create": true}, + expectSuccess: true, + expectedChecks: 2, // Should check both folders + expectedError: 0, + }, + { + name: "Move resource - source folder access denied", + initialFolder: "folder1", + targetFolder: "folder2", + permissionMap: map[string]bool{"folder1:update": false, "folder2:create": true}, + expectSuccess: false, + expectedChecks: 1, // Should stop at first check + expectedError: http.StatusForbidden, + }, + { + name: "Move resource - destination folder access denied", + initialFolder: "folder1", + targetFolder: "folder2", + permissionMap: map[string]bool{"folder1:update": true, "folder2:create": false}, + expectSuccess: false, + expectedChecks: 2, // Should do both checks but fail on second + expectedError: http.StatusForbidden, + }, + { + name: "Move resource - from empty folder to named folder", + initialFolder: "", + targetFolder: "folder1", + permissionMap: map[string]bool{":update": true, "folder1:create": true}, + expectSuccess: true, + expectedChecks: 2, + expectedError: 0, + }, + } + + for i, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a unique resource name for each test + resourceName := fmt.Sprintf("test-resource-%d", i) + resourceUID := fmt.Sprintf("test123-%d", i) + + // Create a mock access client with the test case's permission map + checksPerformed := []types.CheckRequest{} + mockAccess := &mockAccessClient{ + allowed: false, // Default to false + allowedMap: tc.permissionMap, + checkFn: func(req types.CheckRequest) { + checksPerformed = append(checksPerformed, req) + }, + } + + server, err := resource.NewResourceServer(resource.ResourceServerOptions{ + Backend: backend, + AccessClient: mockAccess, + }) + require.NoError(t, err) + + ctx := types.WithAuthInfo(context.Background(), testUser) + + key := &resource.ResourceKey{ + Group: "test.grafana.app", + Resource: "testresources", + Namespace: nsPrefix + "-ns1", + Name: resourceName, + } + + if tc.targetFolder == "" { + // Create resource with unique name + resourceJSON := fmt.Sprintf(`{ + "apiVersion": "test.grafana.app/v1", + "kind": "TestResource", + "metadata": { + "name": "%s", + "uid": "%s", + "namespace": "%s", + "annotations": { + "grafana.app/folder": "%s" + } + }, + "spec": { + "title": "Test Resource %d" + } + }`, resourceName, resourceUID, nsPrefix+"-ns1", tc.initialFolder, i) + + checksPerformed = []types.CheckRequest{} + created, err := server.Create(ctx, &resource.CreateRequest{ + Value: []byte(resourceJSON), + Key: key, + }) + require.NoError(t, err) + + if tc.expectSuccess { + require.Nil(t, created.Error) + require.True(t, created.ResourceVersion > 0) + } else { + require.NotNil(t, created.Error) + require.Equal(t, tc.expectedError, created.Error.Code) + } + + require.Len(t, checksPerformed, tc.expectedChecks) + if len(checksPerformed) > 0 { + require.Equal(t, utils.VerbCreate, checksPerformed[0].Verb) + require.Equal(t, tc.initialFolder, checksPerformed[0].Folder) + } + } else { + // Create a resource first, then update/move it + initialResourceJSON := fmt.Sprintf(`{ + "apiVersion": "test.grafana.app/v1", + "kind": "TestResource", + "metadata": { + "name": "%s", + "uid": "%s", + "namespace": "%s", + "annotations": { + "grafana.app/folder": "%s" + } + }, + "spec": { + "title": "Test Resource %d" + } + }`, resourceName, resourceUID, nsPrefix+"-ns1", tc.initialFolder, i) + + // Override permissions for initial creation to always succeed + mockAccess.allowed = true + created, err := server.Create(ctx, &resource.CreateRequest{ + Value: []byte(initialResourceJSON), + Key: key, + }) + require.NoError(t, err) + require.Nil(t, created.Error) + + // Now try the update/move with the configured permissions + targetResourceJSON := fmt.Sprintf(`{ + "apiVersion": "test.grafana.app/v1", + "kind": "TestResource", + "metadata": { + "name": "%s", + "uid": "%s", + "namespace": "%s", + "annotations": { + "grafana.app/folder": "%s" + } + }, + "spec": { + "title": "Test Resource %d Updated" + } + }`, resourceName, resourceUID, nsPrefix+"-ns1", tc.targetFolder, i) + + mockAccess.allowed = false // Reset to use the map + checksPerformed = []types.CheckRequest{} + + updated, err := server.Update(ctx, &resource.UpdateRequest{ + Key: key, + Value: []byte(targetResourceJSON), + ResourceVersion: created.ResourceVersion, + }) + require.NoError(t, err) + + if tc.expectSuccess { + require.Nil(t, updated.Error) + require.True(t, updated.ResourceVersion > created.ResourceVersion) + } else { + require.NotNil(t, updated.Error) + require.Equal(t, tc.expectedError, updated.Error.Code) + } + + require.Len(t, checksPerformed, tc.expectedChecks) + + // Verify the correct permission checks were made + if tc.initialFolder != tc.targetFolder && len(checksPerformed) >= 2 { + // This is a folder move operation + require.Equal(t, utils.VerbUpdate, checksPerformed[0].Verb) + require.Equal(t, tc.initialFolder, checksPerformed[0].Folder) + + require.Equal(t, utils.VerbCreate, checksPerformed[1].Verb) + require.Equal(t, tc.targetFolder, checksPerformed[1].Folder) + } else if len(checksPerformed) > 0 { + // Regular update, no folder change + require.Equal(t, utils.VerbUpdate, checksPerformed[0].Verb) + require.Equal(t, tc.initialFolder, checksPerformed[0].Folder) + } + } + }) + } +} + +// Mock access client for testing +type mockAccessClient struct { + allowed bool + allowedMap map[string]bool + checkFn func(types.CheckRequest) +} + +func (m *mockAccessClient) Check(ctx context.Context, user types.AuthInfo, req types.CheckRequest) (types.CheckResponse, error) { + if m.checkFn != nil { + m.checkFn(req) + } + + // Check specific folder:verb mappings if provided + if m.allowedMap != nil { + key := fmt.Sprintf("%s:%s", req.Folder, req.Verb) + if allowed, exists := m.allowedMap[key]; exists { + return types.CheckResponse{Allowed: allowed}, nil + } + } + + return types.CheckResponse{Allowed: m.allowed}, nil +} + +func (m *mockAccessClient) Compile(ctx context.Context, user types.AuthInfo, req types.ListRequest) (types.ItemChecker, error) { + return func(name, folder string) bool { + key := fmt.Sprintf("%s:%s", folder, req.Verb) + if allowed, exists := m.allowedMap[key]; exists { + return allowed + } + return m.allowed + }, nil +} diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index a81a69a95fa..e6e831dabc3 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/util/testutil" @@ -26,13 +27,14 @@ import ( // Test names for the storage backend test suite const ( - TestHappyPath = "happy path" - TestWatchWriteEvents = "watch write events from latest" - TestList = "list" - TestBlobSupport = "blob support" - TestGetResourceStats = "get resource stats" - TestListHistory = "list history" - TestCreateNewResource = "create new resource" + TestHappyPath = "happy path" + TestWatchWriteEvents = "watch write events from latest" + TestList = "list" + TestBlobSupport = "blob support" + TestGetResourceStats = "get resource stats" + TestListHistory = "list history" + TestListHistoryErrorReporting = "list history error reporting" + TestCreateNewResource = "create new resource" ) type NewBackendFunc func(ctx context.Context) resource.StorageBackend @@ -75,6 +77,7 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp {TestBlobSupport, runTestIntegrationBlobSupport}, {TestGetResourceStats, runTestIntegrationBackendGetResourceStats}, {TestListHistory, runTestIntegrationBackendListHistory}, + {TestListHistoryErrorReporting, runTestIntegrationBackendListHistoryErrorReporting}, {TestCreateNewResource, runTestIntegrationBackendCreateNewResource}, } @@ -476,7 +479,7 @@ func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend } func runTestIntegrationBackendListHistory(t *testing.T, backend resource.StorageBackend, nsPrefix string) { - ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) + ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) server := newServer(t, backend) ns := nsPrefix + "-ns1" rv1, _ := writeEvent(ctx, backend, "item1", resource.WatchEvent_ADDED, WithNamespace(ns)) @@ -839,6 +842,58 @@ func runTestIntegrationBackendListHistory(t *testing.T, backend resource.Storage }) } +func runTestIntegrationBackendListHistoryErrorReporting(t *testing.T, backend resource.StorageBackend, nsPrefix string) { + ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second)) + server := newServer(t, backend) + + ns := nsPrefix + "-short" + const ( + name = "it1" + group = "group" + resourceName = "resource" + ) + + start := time.Now() + origRv, _ := writeEvent(ctx, backend, name, resource.WatchEvent_ADDED, WithNamespace(ns), WithGroup(group), WithResource(resourceName)) + require.Greater(t, origRv, int64(0)) + + const events = 500 + prevRv := origRv + for i := 0; i < events; i++ { + rv, err := writeEvent(ctx, backend, name, resource.WatchEvent_MODIFIED, WithNamespace(ns), WithGroup(group), WithResource(resourceName)) + require.NoError(t, err) + require.Greater(t, rv, prevRv) + prevRv = rv + } + t.Log("added events in ", time.Since(start)) + + req := &resource.ListRequest{ + Limit: 2 * events, + Source: resource.ListRequest_HISTORY, + ResourceVersion: origRv, + VersionMatchV2: resource.ResourceVersionMatchV2_NotOlderThan, + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: ns, + Group: group, + Resource: resourceName, + Name: name, + }, + }, + } + + shortContext, cancel := context.WithTimeout(ctx, 1*time.Millisecond) + defer cancel() + + res, err := server.List(shortContext, req) + // We expect context deadline error, but it may be reported as a res.Error object. + t.Log("list error:", err) + if res != nil { + t.Log("iterator error:", res.Error) + } + require.True(t, err != nil || (res != nil && res.Error != nil)) +} + func runTestIntegrationBlobSupport(t *testing.T, backend resource.StorageBackend, nsPrefix string) { ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) server := newServer(t, backend) diff --git a/pkg/tests/api/alerting/api_admin_configuration_test.go b/pkg/tests/api/alerting/api_admin_configuration_test.go index e8ea8aafcfc..bcc603b6962 100644 --- a/pkg/tests/api/alerting/api_admin_configuration_test.go +++ b/pkg/tests/api/alerting/api_admin_configuration_test.go @@ -10,8 +10,6 @@ import ( "testing" "time" - "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" @@ -354,49 +352,3 @@ func TestIntegrationAdminConfiguration_SendingToExternalAlertmanagers(t *testing }, 16*time.Second, 8*time.Second) // the sync interval is 2s so after 8s all alertmanagers (if any) most probably are started } } - -func TestIntegrationAdminConfiguration_CannotCreateInhibitionRules(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - AppModeProduction: true, - }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "admin", - Login: "admin", - }) - client := newAlertingApiClient(grafanaListedAddr, "admin", "admin") - - cfg := apimodels.PostableUserConfig{ - AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{ - Route: &apimodels.Route{ - Receiver: "test", - }, - InhibitRules: []config.InhibitRule{{ - SourceMatchers: config.Matchers{{ - Type: labels.MatchEqual, - Name: "foo", - Value: "bar", - }}, - TargetMatchers: config.Matchers{{ - Type: labels.MatchEqual, - Name: "bar", - Value: "baz", - }}, - }}, - }, - Receivers: []*apimodels.PostableApiReceiver{{ - Receiver: config.Receiver{ - Name: "test", - }, - }}, - }, - } - ok, err := client.PostConfiguration(t, cfg) - require.False(t, ok) - require.EqualError(t, err, "inhibition rules are not supported") -} diff --git a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go b/pkg/tests/api/alerting/api_alertmanager_configuration_test.go deleted file mode 100644 index f8f95fbc58c..00000000000 --- a/pkg/tests/api/alerting/api_alertmanager_configuration_test.go +++ /dev/null @@ -1,554 +0,0 @@ -package alerting - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "regexp" - "testing" - "time" - - "github.com/prometheus/alertmanager/config" - "github.com/prometheus/alertmanager/pkg/labels" - "github.com/prometheus/alertmanager/timeinterval" - "github.com/prometheus/common/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/services/featuremgmt" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/org/orgimpl" - "github.com/grafana/grafana/pkg/services/quota/quotatest" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/tests/testinfra" -) - -func TestIntegrationAlertmanagerConfiguration(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - AppModeProduction: true, - }) - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "admin", - Login: "admin", - }) - client := newAlertingApiClient(grafanaListedAddr, "admin", "admin") - - cases := []struct { - name string - cfg apimodels.PostableUserConfig - expErr string - }{{ - name: "configuration with default route", - cfg: apimodels.PostableUserConfig{ - AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{ - Route: &apimodels.Route{ - Receiver: "test", - }, - }, - Receivers: []*apimodels.PostableApiReceiver{{ - Receiver: config.Receiver{ - Name: "test", - }, - }}, - }, - }, - }, { - name: "configuration with UTF-8 matchers", - cfg: apimodels.PostableUserConfig{ - AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{ - Route: &apimodels.Route{ - Receiver: "test", - Routes: []*apimodels.Route{{ - GroupBy: []model.LabelName{"foo🙂"}, - Matchers: config.Matchers{{ - Type: labels.MatchEqual, - Name: "foo🙂", - Value: "bar", - }, { - Type: labels.MatchNotEqual, - Name: "_bar1", - Value: "baz🙂", - }, { - Type: labels.MatchRegexp, - Name: "0baz", - Value: "[a-zA-Z0-9]+,?", - }, { - Type: labels.MatchNotRegexp, - Name: "corge", - Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$", - }, { - Type: labels.MatchEqual, - Name: "Προμηθέας", // Prometheus in Greek - Value: "Prom", - }, { - Type: labels.MatchNotEqual, - Name: "犬", // Dog in Japanese - Value: "Shiba Inu", - }}, - }}, - }, - }, - Receivers: []*apimodels.PostableApiReceiver{{ - Receiver: config.Receiver{ - Name: "test", - }, - }}, - }, - }, - }, { - name: "configuration with UTF-8 object matchers", - cfg: apimodels.PostableUserConfig{ - AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{ - Route: &apimodels.Route{ - Receiver: "test", - Routes: []*apimodels.Route{{ - GroupBy: []model.LabelName{"foo🙂"}, - ObjectMatchers: apimodels.ObjectMatchers{{ - Type: labels.MatchEqual, - Name: "foo🙂", - Value: "bar", - }, { - Type: labels.MatchNotEqual, - Name: "_bar1", - Value: "baz🙂", - }, { - Type: labels.MatchRegexp, - Name: "0baz", - Value: "[a-zA-Z0-9]+,?", - }, { - Type: labels.MatchNotRegexp, - Name: "corge", - Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$", - }, { - Type: labels.MatchEqual, - Name: "Προμηθέας", // Prometheus in Greek - Value: "Prom", - }, { - Type: labels.MatchNotEqual, - Name: "犬", // Dog in Japanese - Value: "Shiba Inu", - }}, - }}, - }, - }, - Receivers: []*apimodels.PostableApiReceiver{{ - Receiver: config.Receiver{ - Name: "test", - }, - }}, - }, - }, - }, { - name: "configuration with UTF-8 in both matchers and object matchers", - cfg: apimodels.PostableUserConfig{ - AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{ - Route: &apimodels.Route{ - Receiver: "test", - Routes: []*apimodels.Route{{ - GroupBy: []model.LabelName{"foo🙂"}, - Matchers: config.Matchers{{ - Type: labels.MatchEqual, - Name: "foo🙂", - Value: "bar", - }, { - Type: labels.MatchNotEqual, - Name: "_bar1", - Value: "baz🙂", - }, { - Type: labels.MatchRegexp, - Name: "0baz", - Value: "[a-zA-Z0-9]+,?", - }, { - Type: labels.MatchNotRegexp, - Name: "corge", - Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$", - }}, - ObjectMatchers: apimodels.ObjectMatchers{{ - Type: labels.MatchEqual, - Name: "Προμηθέας", // Prometheus in Greek - Value: "Prom", - }, { - Type: labels.MatchNotEqual, - Name: "犬", // Dog in Japanese - Value: "Shiba Inu", - }}, - }}, - }, - }, - Receivers: []*apimodels.PostableApiReceiver{{ - Receiver: config.Receiver{ - Name: "test", - }, - }}, - }, - }, - }, { - // TODO: Mute time intervals is deprecated in Alertmanager and scheduled to be - // removed before version 1.0. Remove this test when support for mute time - // intervals is removed. - name: "configuration with mute time intervals", - cfg: apimodels.PostableUserConfig{ - AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{ - Route: &apimodels.Route{ - Receiver: "test", - Routes: []*apimodels.Route{{ - MuteTimeIntervals: []string{"weekends"}, - }}, - }, - MuteTimeIntervals: []config.MuteTimeInterval{{ - Name: "weekends", - TimeIntervals: []timeinterval.TimeInterval{{ - Weekdays: []timeinterval.WeekdayRange{{ - InclusiveRange: timeinterval.InclusiveRange{ - Begin: 1, - End: 5, - }, - }}, - }}, - }}, - }, - Receivers: []*apimodels.PostableApiReceiver{{ - Receiver: config.Receiver{ - Name: "test", - }, - }}, - }, - }, - }, { - name: "configuration with time intervals", - cfg: apimodels.PostableUserConfig{ - AlertmanagerConfig: apimodels.PostableApiAlertingConfig{ - Config: apimodels.Config{ - Route: &apimodels.Route{ - Receiver: "test", - Routes: []*apimodels.Route{{ - MuteTimeIntervals: []string{"weekends"}, - }}, - }, - TimeIntervals: []config.TimeInterval{{ - Name: "weekends", - TimeIntervals: []timeinterval.TimeInterval{{ - Weekdays: []timeinterval.WeekdayRange{{ - InclusiveRange: timeinterval.InclusiveRange{ - Begin: 1, - End: 5, - }, - }}, - }}, - }}, - }, - Receivers: []*apimodels.PostableApiReceiver{{ - Receiver: config.Receiver{ - Name: "test", - }, - }}, - }, - }, - }} - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ok, err := client.PostConfiguration(t, tc.cfg) - if tc.expErr != "" { - require.EqualError(t, err, tc.expErr) - require.False(t, ok) - } else { - require.NoError(t, err) - require.True(t, ok) - } - }) - } -} - -func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) - - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - NGAlertAlertmanagerConfigPollInterval: 2 * time.Second, - DisableAnonymous: true, - AppModeProduction: true, - }) - - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - - orgService, err := orgimpl.ProvideService(env.SQLStore, env.Cfg, quotatest.New(false, nil)) - require.NoError(t, err) - - // editor from main organisation requests configuration - alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - - // create user under main organisation - userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleEditor), - Password: "editor", - Login: "editor", - }) - - // create another organisation - newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "another org", UserID: userID}) - require.NoError(t, err) - orgID := newOrg.ID - - // create user under different organisation - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleEditor), - Password: "editor-42", - Login: "editor-42", - OrgID: orgID, - }) - - // On a blank start with no configuration, it saves and delivers the default configuration. - { - resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint - require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body)) - } - - // When creating new configuration, if it fails to apply - it does not save it. - { - payload := ` -{ - "template_files": {}, - "alertmanager_config": { - "route": { - "receiver": "slack.receiver" - }, - "receivers": [{ - "name": "slack.receiver", - "grafana_managed_receiver_configs": [{ - "settings": { - "iconEmoji": "", - "iconUrl": "", - "mentionGroups": "", - "mentionUsers": "", - "recipient": "#unified-alerting-test", - "username": "" - }, - "secureSettings": {}, - "type": "slack", - "name": "slack.receiver", - "disableResolveMessage": false, - "uid": "" - }] - }] - } -} -` - resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint - b, err := io.ReadAll(resp.Body) - require.NoError(t, err) - var res map[string]any - require.NoError(t, json.Unmarshal(b, &res)) - require.Regexp(t, `^failed to save and apply Alertmanager configuration: failed to validate integration "slack.receiver" \(UID [^\)]+\) of type "slack": token must be specified when using the Slack chat API`, res["message"]) - resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint - - require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body)) - } - - // editor42 from organisation 42 posts configuration - alertConfigURL = fmt.Sprintf("http://editor-42:editor-42@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - - // Before we start operating, make sure we've synced this org. - require.Eventually(t, func() bool { - resp, err := http.Get(alertConfigURL) // nolint - require.NoError(t, err) - return resp.StatusCode == http.StatusOK - }, 10*time.Second, 2*time.Second) - - // Post the alertmanager config. - { - mockChannel := newMockNotificationChannel(t, grafanaListedAddr) - amConfig := getAlertmanagerConfig(mockChannel.server.Addr) - postRequest(t, alertConfigURL, amConfig, http.StatusAccepted) // nolint - - // Verifying that the new configuration is returned - resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint - b := getBody(t, resp.Body) - re := regexp.MustCompile(`"uid":"([\w|-]*)"`) - e := getExpAlertmanagerConfigFromAPI(mockChannel.server.Addr) - require.JSONEq(t, e, string(re.ReplaceAll([]byte(b), []byte(`"uid":""`)))) - } - - // verify that main organisation still gets the default configuration - alertConfigURL = fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - { - resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint - require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body)) - } -} - -func TestIntegrationAlertmanagerConfigurationPersistSecrets(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) - - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - DisableFeatureToggles: []string{featuremgmt.FlagAlertingApiServer}, - }) - - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleEditor), - Password: "editor", - Login: "editor", - }) - generatedUID := "" - - // create a new configuration that has a secret - { - payload := ` -{ - "template_files": {}, - "alertmanager_config": { - "route": { - "receiver": "slack.receiver" - }, - "receivers": [{ - "name": "slack.receiver", - "grafana_managed_receiver_configs": [{ - "settings": { - "recipient": "#unified-alerting-test" - }, - "secureSettings": { - "url": "http://averysecureurl.com/webhook" - }, - "type": "slack", - "name": "slack.receiver", - "disableResolveMessage": false - }] - }] - } -} -` - resp := postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint - require.JSONEq(t, `{"message":"configuration created"}`, getBody(t, resp.Body)) - } - - // Try to update a receiver with unknown UID - { - // Then, update the recipient - payload := ` - { - "template_files": {}, - "alertmanager_config": { - "route": { - "receiver": "slack.receiver" - }, - "receivers": [{ - "name": "slack.receiver", - "grafana_managed_receiver_configs": [{ - "settings": { - "recipient": "#unified-alerting-test-but-updated" - }, - "secureFields": { - "url": true - }, - "type": "slack", - "name": "slack.receiver", - "disableResolveMessage": false, - "uid": "invalid" - }] - }] - } - } - ` - - resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint - s := getBody(t, resp.Body) - var res map[string]any - require.NoError(t, json.Unmarshal([]byte(s), &res)) - require.Equal(t, "unknown receiver: invalid", res["message"]) - } - - // The secure settings must be present - { - resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint - var c apimodels.GettableUserConfig - bb := getBody(t, resp.Body) - err := json.Unmarshal([]byte(bb), &c) - require.NoError(t, err) - m := c.GetGrafanaReceiverMap() - assert.Len(t, m, 1) - for k := range m { - generatedUID = m[k].UID - } - - // Then, update the recipient - payload := fmt.Sprintf(` -{ - "template_files": {}, - "alertmanager_config": { - "route": { - "receiver": "slack.receiver" - }, - "receivers": [{ - "name": "slack.receiver", - "grafana_managed_receiver_configs": [{ - "settings": { - "recipient": "#unified-alerting-test-but-updated" - }, - "secureFields": { - "url": true - }, - "type": "slack", - "name": "slack.receiver", - "disableResolveMessage": false, - "uid": %q - }] - }] - } -} -`, generatedUID) - - resp = postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint - require.JSONEq(t, `{"message": "configuration created"}`, getBody(t, resp.Body)) - } - - // The secure settings must be present - { - resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint - require.JSONEq(t, fmt.Sprintf(` -{ - "template_files": {}, - "alertmanager_config": { - "route": { - "receiver": "slack.receiver" - }, - "receivers": [{ - "name": "slack.receiver", - "grafana_managed_receiver_configs": [{ - "uid": %q, - "name": "slack.receiver", - "type": "slack", - "disableResolveMessage": false, - "settings": { - "recipient": "#unified-alerting-test-but-updated" - }, - "secureFields": { - "url": true - } - }] - }] - } -} -`, generatedUID), getBody(t, resp.Body)) - } -} diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index 4bf9d97c980..f5b0de11398 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -2,6 +2,7 @@ package alerting import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -16,7 +17,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/errutil" - "github.com/grafana/grafana/pkg/services/featuremgmt" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -32,9 +32,6 @@ func TestIntegrationAMConfigAccess(t *testing.T) { EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, - DisableFeatureToggles: []string{ - featuremgmt.FlagAlertingApiServer, - }, }) grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) @@ -63,8 +60,9 @@ func TestIntegrationAMConfigAccess(t *testing.T) { expBody string } - t.Run("when creating alertmanager configuration", func(t *testing.T) { - body := ` + // Create alertmanager config + cfg := apimodels.PostableUserConfig{} + amConfig := ` { "alertmanager_config": { "route": { @@ -85,51 +83,10 @@ func TestIntegrationAMConfigAccess(t *testing.T) { } } ` - - testCases := []testCase{ - { - desc: "un-authenticated request should fail", - url: "http://%s/api/alertmanager/grafana/config/api/v1/alerts", - expStatus: http.StatusUnauthorized, - expBody: `"message":"Unauthorized"`, - }, - { - desc: "viewer request should fail", - url: "http://viewer:viewer@%s/api/alertmanager/grafana/config/api/v1/alerts", - expStatus: http.StatusForbidden, - expBody: `"title":"Access denied"`, - }, - { - desc: "editor request should succeed", - url: "http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", - expStatus: http.StatusAccepted, - expBody: `{"message":"configuration created"}`, - }, - { - desc: "admin request should succeed", - url: "http://admin:admin@%s/api/alertmanager/grafana/config/api/v1/alerts", - expStatus: http.StatusAccepted, - expBody: `{"message":"configuration created"}`, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - url := fmt.Sprintf(tc.url, grafanaListedAddr) - buf := bytes.NewReader([]byte(body)) - // nolint:gosec - resp, err := http.Post(url, "application/json", buf) - t.Cleanup(func() { - require.NoError(t, resp.Body.Close()) - }) - require.NoError(t, err) - require.Equal(t, tc.expStatus, resp.StatusCode) - b, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.Contains(t, string(b), tc.expBody) - }) - } - }) + err := json.Unmarshal([]byte(amConfig), &cfg) + require.NoError(t, err) + err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg) + require.NoError(t, err) t.Run("when retrieve alertmanager configuration", func(t *testing.T) { cfgTemplate := ` @@ -329,7 +286,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) { }) var silences apimodels.GettableSilences - err := json.Unmarshal(blob, &silences) + err = json.Unmarshal(blob, &silences) require.NoError(t, err) assert.Len(t, silences, 2) silenceIDs := make([]string, 0, len(silences)) diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index b653b3a8184..d41ed71b9df 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/expr" - "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/infra/db" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -131,10 +131,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -214,111 +219,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" - }, - "labels": { - "alertname": "TestAlert", - "instance": "Grafana" - } - }, - "receivers": [{ - "name":"receiver-1", - "grafana_managed_receiver_configs": [ - { - "name": "receiver-1", - "uid": "%s", - "status": "ok" - } - ] - }], - "notified_at": "%s" - }`, - result.Receivers[0].Configs[0].UID, - result.NotifiedAt.Format(time.RFC3339Nano)) - require.JSONEq(t, expectedJSON, string(b)) - }) - - t.Run("assert working receiver with existing secure settings returns OK", func(t *testing.T) { - // Setup Grafana and its Database - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - AppModeProduction: true, - }) - - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleEditor), - Login: "grafana", - Password: "password", - }) - - mockChannel := newMockNotificationChannel(t, grafanaListedAddr) - amConfig := createAlertmanagerConfig(`{ - "alertmanager_config": { - "route": { - "receiver": "receiver-1" - }, - "receivers": [{ - "name":"receiver-1", - "grafana_managed_receiver_configs": [ - { - "uid": "", - "name": "receiver-1", - "type": "slack", - "disableResolveMessage": false, - "settings": {}, - "secureSettings": {"url": "http://CHANNEL_ADDR/slack_recv1/slack_test_without_token"} - } - ] - }] - } - }`, mockChannel.server.Addr) - - // Set up responses - mockChannel.responses["slack_recv1"] = `{"ok": true}` - - // Post config - u := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - _ = postRequest(t, u, amConfig, http.StatusAccepted) // nolint - - // Get am config with UID and without secureSettings - resp := getRequest(t, u, http.StatusOK) - b, err := io.ReadAll(resp.Body) - require.NoError(t, err) - config, err := notifier.Load(b) - require.NoError(t, err) - body, err := json.Marshal(apimodels.TestReceiversConfigBodyParams{ - Receivers: config.AlertmanagerConfig.Receivers, - }) - require.NoError(t, err) - - // Test using the am config without secureSettings - testReceiversURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/receivers/test", grafanaListedAddr) - // nolint - resp = postRequest(t, testReceiversURL, string(body), http.StatusOK) - t.Cleanup(func() { - err := resp.Body.Close() - require.NoError(t, err) - }) - - b, err = io.ReadAll(resp.Body) - require.NoError(t, err) - - var result apimodels.TestReceiversResult - require.NoError(t, json.Unmarshal(b, &result)) - require.Len(t, result.Receivers, 1) - require.Len(t, result.Receivers[0].Configs, 1) - - expectedJSON := fmt.Sprintf(`{ - "alert": { - "annotations": { - "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -392,10 +301,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -480,10 +394,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -581,10 +500,15 @@ func TestIntegrationTestReceivers(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -687,10 +611,15 @@ func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { "annotations": { "annotation1": "value1", "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana", "label1": "value1" } @@ -776,10 +705,15 @@ func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { "alert": { "annotations": { "summary": "This is a custom annotation", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "TestAlert", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -863,10 +797,15 @@ func TestIntegrationTestReceiversAlertCustomization(t *testing.T) { "alert": { "annotations": { "summary": "Notification test", - "__value_string__": "[ metric='foo' labels={instance=bar} value=10 ]" + "__dashboardUid__": "dashboard_uid", + "__orgId__": "1", + "__panelId__": "1", + "__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]", + "__values__": "{\"B\":22,\"C\":1}" }, "labels": { "alertname": "This is a custom label", + "grafana_folder": "Test Folder", "instance": "Grafana" } }, @@ -962,8 +901,11 @@ func TestIntegrationNotificationChannels(t *testing.T) { apiClient.CreateFolder(t, "default", "default") // Post the alertmanager config. - u := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) - _ = postRequest(t, u, amConfig, http.StatusAccepted) // nolint + cfg := apimodels.PostableUserConfig{} + err := json.Unmarshal([]byte(amConfig), &cfg) + require.NoError(t, err) + err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg) + require.NoError(t, err) // Verifying that all the receivers and routes have been registered. alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) @@ -979,7 +921,7 @@ func TestIntegrationNotificationChannels(t *testing.T) { b = getBody(t, resp.Body) var receivers []apimodels.Receiver - err := json.Unmarshal([]byte(b), &receivers) + err = json.Unmarshal([]byte(b), &receivers) require.NoError(t, err) for _, rcv := range receivers { require.NotNil(t, rcv.Name) @@ -2491,6 +2433,7 @@ var expEmailNotifications = []*notifications.SendEmailCommandSync{ SilenceURL: "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%3DUID_EmailAlert&orgId=1", DashboardURL: "", PanelURL: "", + OrgID: util.Pointer(int64(1)), Values: map[string]float64{"A": 1}, ValueString: "[ var='A' labels={} value=1 ]", }, @@ -2654,7 +2597,8 @@ var expNonEmailNotifications = map[string][]string{ "fingerprint": "15c59b0a380bd9f1", "silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=__alert_rule_uid__%%3DUID_WebhookAlert&orgId=1", "dashboardURL": "", - "panelURL": "" + "panelURL": "", + "orgId": 1 } ], "groupLabels": { diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index a25cde45fb4..1114d1102b2 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -16,7 +16,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/expr" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -25,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util" ) // Declare respModel at the function level @@ -194,6 +194,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { }`), }, }, + IsPaused: util.Pointer(true), }, }, }, diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 8eeb3450641..4dec75a1021 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -3288,7 +3288,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "intervalSeconds":60, "is_paused": false, "version":1, - "uid":"uid", + "uid":"uid", "guid": "guid", "namespace_uid":"nsuid", "rule_group":"arulegroup", @@ -4451,32 +4451,6 @@ func TestIntegrationRuleNotificationSettings(t *testing.T) { t.Log(body) }) - t.Run("create with '...' groupBy followed by config post should succeed", func(t *testing.T) { - var copyD testData - err = json.Unmarshal(testDataRaw, ©D) - group := copyD.RuleGroup - ns := group.Rules[0].GrafanaManagedAlert.NotificationSettings - ns.GroupBy = []string{ngmodels.FolderTitleLabel, model.AlertNameLabel, ngmodels.GroupByAll} - - _, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &group, false) - require.Equalf(t, http.StatusAccepted, status, body) - - // Now update the config with no changes. - _, status, body = apiClient.GetAlertmanagerConfigWithStatus(t) - if !assert.Equalf(t, http.StatusOK, status, body) { - return - } - - cfg := apimodels.PostableUserConfig{} - - err = json.Unmarshal([]byte(body), &cfg) - require.NoError(t, err) - - ok, err := apiClient.PostConfiguration(t, cfg) - require.NoError(t, err) - require.True(t, ok) - }) - t.Run("should create rule and generate route", func(t *testing.T) { _, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &d.RuleGroup, false) require.Equalf(t, http.StatusAccepted, status, body) diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index e42bfb3a546..f79fe7f3ec6 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -420,40 +419,6 @@ func (a apiClient) UpdateAlertRuleOrgQuota(t *testing.T, orgID int64, limit int6 assert.Equal(t, http.StatusOK, resp.StatusCode) } -func (a apiClient) PostConfiguration(t *testing.T, c apimodels.PostableUserConfig) (bool, error) { - t.Helper() - - b, err := json.Marshal(c) - require.NoError(t, err) - - u := fmt.Sprintf("%s/api/alertmanager/grafana/config/api/v1/alerts", a.url) - req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - require.NotNil(t, resp) - - defer func() { - _ = resp.Body.Close() - }() - b, err = io.ReadAll(resp.Body) - require.NoError(t, err) - - data := struct { - Message string `json:"message"` - }{} - require.NoError(t, json.Unmarshal(b, &data)) - - if resp.StatusCode == http.StatusAccepted { - return true, nil - } - - return false, errors.New(data.Message) -} - func (a apiClient) PostRulesGroupWithStatus(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig, permanentlyDelete bool) (apimodels.UpdateRuleGroupResponse, int, string) { t.Helper() buf := bytes.Buffer{} diff --git a/pkg/tests/apis/alerting/notifications/common/testing.go b/pkg/tests/apis/alerting/notifications/common/testing.go new file mode 100644 index 00000000000..167602a66bf --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/common/testing.go @@ -0,0 +1,79 @@ +package common + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + + v0alpha1_receiver "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1" + v0alpha1_routingtree "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1" + v0alpha1_templategroup "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1" + v0alpha1_timeinterval "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1" +) + +func NewReceiverClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_receiver.Receiver, v0alpha1_receiver.ReceiverList] { + t.Helper() + + client, err := dynamic.NewForConfig(user.NewRestConfig()) + require.NoError(t, err) + + return &apis.TypedClient[v0alpha1_receiver.Receiver, v0alpha1_receiver.ReceiverList]{ + Client: client.Resource( + schema.GroupVersionResource{ + Group: v0alpha1_receiver.Kind().Group(), + Version: v0alpha1_receiver.Kind().Version(), + Resource: v0alpha1_receiver.Kind().Plural(), + }).Namespace("default"), + } +} + +func NewRoutingTreeClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_routingtree.RoutingTree, v0alpha1_routingtree.RoutingTreeList] { + t.Helper() + + client, err := dynamic.NewForConfig(user.NewRestConfig()) + require.NoError(t, err) + + return &apis.TypedClient[v0alpha1_routingtree.RoutingTree, v0alpha1_routingtree.RoutingTreeList]{ + Client: client.Resource( + schema.GroupVersionResource{ + Group: v0alpha1_routingtree.Kind().Group(), + Version: v0alpha1_routingtree.Kind().Version(), + Resource: v0alpha1_routingtree.Kind().Plural(), + }).Namespace("default"), + } +} + +func NewTemplateGroupClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_templategroup.TemplateGroup, v0alpha1_templategroup.TemplateGroupList] { + t.Helper() + + client, err := dynamic.NewForConfig(user.NewRestConfig()) + require.NoError(t, err) + + return &apis.TypedClient[v0alpha1_templategroup.TemplateGroup, v0alpha1_templategroup.TemplateGroupList]{ + Client: client.Resource( + schema.GroupVersionResource{ + Group: v0alpha1_templategroup.Kind().Group(), + Version: v0alpha1_templategroup.Kind().Version(), + Resource: v0alpha1_templategroup.Kind().Plural(), + }).Namespace("default"), + } +} + +func NewTimeIntervalClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_timeinterval.TimeInterval, v0alpha1_timeinterval.TimeIntervalList] { + t.Helper() + + client, err := dynamic.NewForConfig(user.NewRestConfig()) + require.NoError(t, err) + + return &apis.TypedClient[v0alpha1_timeinterval.TimeInterval, v0alpha1_timeinterval.TimeIntervalList]{ + Client: client.Resource( + schema.GroupVersionResource{ + Group: v0alpha1_timeinterval.Kind().Group(), + Version: v0alpha1_timeinterval.Kind().Version(), + Resource: v0alpha1_timeinterval.Kind().Plural(), + }).Namespace("default"), + } +} diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 34e8a7f3c06..43bc217123b 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -14,18 +14,19 @@ import ( "testing" "github.com/grafana/alerting/notify" - "github.com/prometheus/alertmanager/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/dynamic" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1" + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree" + + test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" @@ -67,7 +68,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - client := newClient(t, helper.Org1.Admin) + client := test_common.NewReceiverClient(t, helper.Org1.Admin) newResource := &v0alpha1.Receiver{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", @@ -145,7 +146,7 @@ func TestIntegrationResourcePermissions(t *testing.T) { admin := org1.Admin viewer := org1.Viewer editor := org1.Editor - adminClient := newClient(t, admin) + adminClient := test_common.NewReceiverClient(t, admin) writeACMetadata := []string{"canWrite", "canDelete"} allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin"} @@ -297,8 +298,8 @@ func TestIntegrationResourcePermissions(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - createClient := newClient(t, tc.creatingUser) - client := newClient(t, tc.testUser) + createClient := test_common.NewReceiverClient(t, tc.creatingUser) + client := test_common.NewReceiverClient(t, tc.testUser) var created = &v0alpha1.Receiver{ ObjectMeta: v1.ObjectMeta{ @@ -561,10 +562,10 @@ func TestIntegrationAccessControl(t *testing.T) { }, } - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) for _, tc := range testCases { t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { - client := newClient(t, tc.user) + client := test_common.NewReceiverClient(t, tc.user) var expected = &v0alpha1.Receiver{ ObjectMeta: v1.ObjectMeta{ @@ -739,7 +740,7 @@ func TestIntegrationInUseMetadata(t *testing.T) { cliCfg := helper.Org1.Admin.NewRestConfig() legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) // Prepare environment and create notification policy and rule that use receiver alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json")) require.NoError(t, err) @@ -754,7 +755,7 @@ func TestIntegrationInUseMetadata(t *testing.T) { parentRoute.Routes = []*definitions.Route{&route1, &route2} amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, &parentRoute) - persistInitialConfig(t, amConfig, adminClient, legacyCli) + persistInitialConfig(t, amConfig) postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json")) require.NoError(t, err) @@ -817,8 +818,11 @@ func TestIntegrationInUseMetadata(t *testing.T) { // Removing the new extra route should leave only 1. amConfig.AlertmanagerConfig.Route.Routes = amConfig.AlertmanagerConfig.Route.Routes[:1] - success, err := legacyCli.PostConfiguration(t, amConfig) - require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err) + v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" }) + require.NoError(t, err) + routeAdminClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin) + _, err = routeAdminClient.Update(ctx, v1Route, v1.UpdateOptions{}) + require.NoError(t, err) receiverListed, receiverGet = requestReceivers(t, "user-defined") checkInUse(t, receiverListed, receiverGet, 1, 2) @@ -834,11 +838,14 @@ func TestIntegrationInUseMetadata(t *testing.T) { receiverListed, receiverGet = requestReceivers(t, "grafana-default-email") checkInUse(t, receiverListed, receiverGet, 1, 0) - // Remove the rest. + // Remove the remaining routes. amConfig.AlertmanagerConfig.Route.Routes = nil - success, err = legacyCli.PostConfiguration(t, amConfig) - require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err) + v1route, err := routingtree.ConvertToK8sResource(1, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" }) + require.NoError(t, err) + _, err = routeAdminClient.Update(ctx, v1route, v1.UpdateOptions{}) + require.NoError(t, err) + // Remove the remaining rules. ruleGroup.Rules = nil _, status, data = legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false) require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data) @@ -861,7 +868,7 @@ func TestIntegrationProvisioning(t *testing.T) { org := helper.Org1 admin := org.Admin - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles) db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) @@ -915,7 +922,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) receiver := v0alpha1.Receiver{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", @@ -998,7 +1005,7 @@ func TestIntegrationPatch(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) receiver := v0alpha1.Receiver{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", @@ -1086,75 +1093,6 @@ func TestIntegrationPatch(t *testing.T) { }) } -func TestIntegrationRejectConfigApiReceiverModification(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - helper := getTestHelper(t) - - cliCfg := helper.Org1.Admin.NewRestConfig() - legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) - - adminClient := newClient(t, helper.Org1.Admin) - - alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json")) - require.NoError(t, err) - var amConfig definitions.PostableUserConfig - require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig)) - - persistInitialConfig(t, amConfig, adminClient, legacyCli) - - // We modify the receiver, this should cause the POST to be rejected. - userDefinedReceiver := amConfig.AlertmanagerConfig.Receivers[slices.IndexFunc(amConfig.AlertmanagerConfig.Receivers, func(receiver *definitions.PostableApiReceiver) bool { - return receiver.Name == "user-defined" - })] - userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage = !userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage - success, err := legacyCli.PostConfiguration(t, amConfig) - require.Falsef(t, success, "Expected receiver modification to be rejected, but got %t", success) - require.ErrorContainsf(t, err, "alertingApiServer", "Expected error message to contain 'alertingApiServer', but got %s", err) - - // Revert the change. - userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage = !userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage - - // We add a receiver, this should be accepted. - amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, &definitions.PostableApiReceiver{ - Receiver: config.Receiver{ - Name: "new receiver", - }, - PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ - GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ - { - Name: "new receiver", - Type: "email", - Settings: []byte(`{"addresses": ""}`), - }, - }, - }, - }) - success, err = legacyCli.PostConfiguration(t, amConfig) - require.Truef(t, success, "Expected receiver modification to be accepted, but got %s", err) - require.NoError(t, err) - - // Sanity check. - gettable, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t) - require.Equalf(t, http.StatusOK, status, body) - require.Lenf(t, gettable.AlertmanagerConfig.Receivers, 3, "Expected 3 receivers, got %d", len(gettable.AlertmanagerConfig.Receivers)) - - // We remove the receiver, this should be accepted. - amConfig.AlertmanagerConfig.Receivers = slices.DeleteFunc(amConfig.AlertmanagerConfig.Receivers, func(receiver *definitions.PostableApiReceiver) bool { - return receiver.GrafanaManagedReceivers[0].Name == "new receiver" - }) - success, err = legacyCli.PostConfiguration(t, amConfig) - require.Truef(t, success, "Expected receiver modification to be accepted, but got %s", err) - require.NoError(t, err) - - // Sanity check. - gettable, status, body = legacyCli.GetAlertmanagerConfigWithStatus(t) - require.Equalf(t, http.StatusOK, status, body) - require.Lenf(t, gettable.AlertmanagerConfig.Receivers, 2, "Expected 2 receivers, got %d", len(gettable.AlertmanagerConfig.Receivers)) -} - func TestIntegrationReferentialIntegrity(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -1171,14 +1109,14 @@ func TestIntegrationReferentialIntegrity(t *testing.T) { cliCfg := helper.Org1.Admin.NewRestConfig() legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) // Prepare environment and create notification policy and rule that use time receiver alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json")) require.NoError(t, err) var amConfig definitions.PostableUserConfig require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig)) - persistInitialConfig(t, amConfig, adminClient, legacyCli) + persistInitialConfig(t, amConfig) postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json")) require.NoError(t, err) @@ -1283,7 +1221,7 @@ func TestIntegrationCRUD(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) var defaultReceiver *v0alpha1.Receiver t.Run("should list the default receiver", func(t *testing.T) { items, err := adminClient.List(ctx, v1.ListOptions{}) @@ -1451,7 +1389,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin) recv1 := &v0alpha1.Receiver{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", @@ -1533,13 +1471,14 @@ func TestIntegrationReceiverListSelector(t *testing.T) { // persistInitialConfig helps create an initial config with new receivers using legacy json. Config API blocks receiver // modifications, so we need to use k8s API to create new receivers before posting the config. -func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig, adminClient *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList], legacyCli alerting.LegacyApiClient) { +func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) { ctx := context.Background() - var defaultReceiver *definitions.PostableApiReceiver + helper := getTestHelper(t) + + receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin) for _, receiver := range amConfig.AlertmanagerConfig.Receivers { if receiver.Name == "grafana-default-email" { - defaultReceiver = receiver continue } @@ -1563,7 +1502,7 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig, }) } - created, err := adminClient.Create(ctx, &toCreate, v1.CreateOptions{}) + created, err := receiverClient.Create(ctx, &toCreate, v1.CreateOptions{}) require.NoError(t, err) for i, integration := range created.Spec.Integrations { @@ -1571,19 +1510,13 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig, } } - success, err := legacyCli.PostConfiguration(t, amConfig) - require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err) + nsMapper := func(_ int64) string { return "default" } - gettable, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t) - require.Equalf(t, http.StatusOK, status, body) - - idx := slices.IndexFunc(gettable.AlertmanagerConfig.Receivers, func(recv *definitions.GettableApiReceiver) bool { - return recv.Name == "grafana-default-email" - }) - gettableDefault := gettable.AlertmanagerConfig.Receivers[idx] - - // Assign uid of default receiver as well. - defaultReceiver.GrafanaManagedReceivers[0].UID = gettableDefault.GrafanaManagedReceivers[0].UID + routeClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin) + v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", nsMapper) + require.NoError(t, err) + _, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{}) + require.NoError(t, err) } func createIntegration(t *testing.T, integrationType string) v0alpha1.Integration { @@ -1609,19 +1542,3 @@ func createWildcardPermission(actions ...string) resourcepermissions.SetResource ResourceID: "*", } } - -func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList] { - t.Helper() - - client, err := dynamic.NewForConfig(user.NewRestConfig()) - require.NoError(t, err) - - return &apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList]{ - Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.Kind().Group(), - Version: v0alpha1.Kind().Version(), - Resource: v0alpha1.Kind().Plural(), - }).Namespace("default"), - } -} diff --git a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go index 69acdea7254..cfd5be4ae93 100644 --- a/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go +++ b/pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/grafana/alerting/definition" "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" @@ -16,10 +15,12 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1" + v0alpha1_timeinterval "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree" + + "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1/fakes" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -32,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/tests/api/alerting" "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" @@ -52,7 +54,7 @@ func TestIntegrationNotAllowedMethods(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - client := newClient(t, helper.Org1.Admin) + client := common.NewRoutingTreeClient(t, helper.Org1.Admin) route := &v0alpha1.RoutingTree{ ObjectMeta: v1.ObjectMeta{ @@ -156,11 +158,11 @@ func TestIntegrationAccessControl(t *testing.T) { } admin := org1.Admin - adminClient := newClient(t, admin) + adminClient := common.NewRoutingTreeClient(t, admin) for _, tc := range testCases { t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { - client := newClient(t, tc.user) + client := common.NewRoutingTreeClient(t, tc.user) if tc.canRead { t.Run("should be able to list routing trees", func(t *testing.T) { @@ -291,7 +293,7 @@ func TestIntegrationProvisioning(t *testing.T) { org := helper.Org1 admin := org.Admin - adminClient := newClient(t, admin) + adminClient := common.NewRoutingTreeClient(t, admin) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles) @@ -342,7 +344,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewRoutingTreeClient(t, helper.Org1.Admin) current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) require.NoError(t, err) @@ -388,46 +390,30 @@ func TestIntegrationDataConsistency(t *testing.T) { cliCfg := helper.Org1.Admin.NewRestConfig() legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password) - client := newClient(t, helper.Org1.Admin) + client := common.NewRoutingTreeClient(t, helper.Org1.Admin) receiver := "grafana-default-email" timeInterval := "test-time-interval" createRoute := func(t *testing.T, route definitions.Route) { t.Helper() - cfg, _, _ := legacyCli.GetAlertmanagerConfigWithStatus(t) - var receivers []*definitions.PostableApiReceiver - for _, apiReceiver := range cfg.AlertmanagerConfig.Receivers { - var recv []*definitions.PostableGrafanaReceiver - for _, r := range apiReceiver.GrafanaManagedReceivers { - recv = append(recv, &definitions.PostableGrafanaReceiver{ - UID: r.UID, - Name: r.Name, - Type: r.Type, - DisableResolveMessage: r.DisableResolveMessage, - Settings: r.Settings, - }) - } - receivers = append(receivers, &definitions.PostableApiReceiver{ - Receiver: config.Receiver{Name: apiReceiver.Name}, - PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{GrafanaManagedReceivers: recv}, - }) - } - _, err := legacyCli.PostConfiguration(t, definitions.PostableUserConfig{ - AlertmanagerConfig: definitions.PostableApiAlertingConfig{ - Config: definition.Config{ - Route: &route, - TimeIntervals: []config.TimeInterval{ - { - Name: timeInterval, - }, - }, - }, - Receivers: receivers, - }, - }) + routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin) + v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), route, "", func(int64) string { return "default" }) + require.NoError(t, err) + _, err = routeClient.Update(ctx, v1Route, v1.UpdateOptions{}) require.NoError(t, err) } + _, err := common.NewTimeIntervalClient(t, helper.Org1.Admin).Create(ctx, &v0alpha1_timeinterval.TimeInterval{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1_timeinterval.Spec{ + Name: timeInterval, + TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(1), + }, + }, v1.CreateOptions{}) + require.NoError(t, err) + var regex config.Regexp require.NoError(t, json.Unmarshal([]byte(`".*"`), ®ex)) @@ -461,7 +447,7 @@ func TestIntegrationDataConsistency(t *testing.T) { createRoute(t, route) tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) require.NoError(t, err) - assert.Equal(t, []v0alpha1.Matcher{ + expected := []v0alpha1.Matcher{ { Label: "label_match", Type: v0alpha1.MatcherTypeEqual, @@ -482,7 +468,8 @@ func TestIntegrationDataConsistency(t *testing.T) { Type: v0alpha1.MatcherTypeNotEqualRegex, Value: "test-456", }, - }, tree.Spec.Routes[0].Matchers) + } + assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers) }) t.Run("should save into ObjectMatchers", func(t *testing.T) { route := definitions.Route{ @@ -623,20 +610,55 @@ func TestIntegrationDataConsistency(t *testing.T) { require.Equalf(t, http.StatusOK, status, body) require.Equal(t, before, after) }) -} -func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.RoutingTree, v0alpha1.RoutingTreeList] { - t.Helper() + t.Run("unicode support in groupBy and matchers", func(t *testing.T) { + route := definitions.Route{ + Receiver: receiver, + Routes: []*definitions.Route{ + { + GroupByStr: []string{"foo🙂"}, + Matchers: config.Matchers{{ + Type: labels.MatchEqual, + Name: "foo🙂", + Value: "bar", + }, { + Type: labels.MatchNotEqual, + Name: "_bar1", + Value: "baz🙂", + }, { + Type: labels.MatchRegexp, + Name: "0baz", + Value: "[a-zA-Z0-9]+,?", + }, { + Type: labels.MatchNotRegexp, + Name: "corge", + Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$", + }}, + ObjectMatchers: definitions.ObjectMatchers{{ + Type: labels.MatchEqual, + Name: "Προμηθέας", // Prometheus in Greek + Value: "Prom", + }, { + Type: labels.MatchNotEqual, + Name: "犬", // Dog in Japanese (inu) + Value: "Shiba Inu", + }}, + }, + }, + } - client, err := dynamic.NewForConfig(user.NewRestConfig()) - require.NoError(t, err) - - return &apis.TypedClient[v0alpha1.RoutingTree, v0alpha1.RoutingTreeList]{ - Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.Kind().Group(), - Version: v0alpha1.Kind().Version(), - Resource: v0alpha1.Kind().Plural(), - }).Namespace("default"), - } + createRoute(t, route) + tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, "foo🙂", tree.Spec.Routes[0].GroupBy[0]) + expected := []v0alpha1.Matcher{ + {Label: "foo🙂", Type: v0alpha1.MatcherTypeEqual, Value: "bar"}, + {Label: "_bar1", Type: v0alpha1.MatcherTypeNotEqual, Value: "baz🙂"}, + {Label: "0baz", Type: v0alpha1.MatcherTypeEqualRegex, Value: "[a-zA-Z0-9]+,?"}, + {Label: "corge", Type: v0alpha1.MatcherTypeNotEqualRegex, Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$"}, + {Label: "Προμηθέας", Type: v0alpha1.MatcherTypeEqual, Value: "Prom"}, + {Label: "犬", Type: v0alpha1.MatcherTypeNotEqual, Value: "Shiba Inu"}, + } + assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers) + }) } diff --git a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go index 8dbaf88f4f2..1e84a658a2d 100644 --- a/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go +++ b/pkg/tests/apis/alerting/notifications/templategroup/templates_group_test.go @@ -11,9 +11,7 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/dynamic" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1" "github.com/grafana/grafana/pkg/bus" @@ -27,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" @@ -47,7 +46,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - client := newClient(t, helper.Org1.Admin) + client := common.NewTemplateGroupClient(t, helper.Org1.Admin) newTemplate := &v0alpha1.TemplateGroup{ ObjectMeta: v1.ObjectMeta{ @@ -217,11 +216,11 @@ func TestIntegrationAccessControl(t *testing.T) { }, } - adminClient := newClient(t, org1.Admin) + adminClient := common.NewTemplateGroupClient(t, org1.Admin) for _, tc := range testCases { t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { - client := newClient(t, tc.user) + client := common.NewTemplateGroupClient(t, tc.user) var expected = &v0alpha1.TemplateGroup{ ObjectMeta: v1.ObjectMeta{ @@ -377,7 +376,7 @@ func TestIntegrationProvisioning(t *testing.T) { org := helper.Org1 admin := org.Admin - adminClient := newClient(t, admin) + adminClient := common.NewTemplateGroupClient(t, admin) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles) @@ -427,7 +426,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin) template := v0alpha1.TemplateGroup{ ObjectMeta: v1.ObjectMeta{ @@ -511,7 +510,7 @@ func TestIntegrationPatch(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin) template := v0alpha1.TemplateGroup{ ObjectMeta: v1.ObjectMeta{ @@ -571,7 +570,7 @@ func TestIntegrationListSelector(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin) template1 := &v0alpha1.TemplateGroup{ ObjectMeta: v1.ObjectMeta{ @@ -664,19 +663,3 @@ func TestIntegrationListSelector(t *testing.T) { require.NotEqualf(t, templates.DefaultTemplateName, list.Items[1].Name, "Expected non-default template but got %s", list.Items[1].Name) }) } - -func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.TemplateGroup, v0alpha1.TemplateGroupList] { - t.Helper() - - client, err := dynamic.NewForConfig(user.NewRestConfig()) - require.NoError(t, err) - - return &apis.TypedClient[v0alpha1.TemplateGroup, v0alpha1.TemplateGroupList]{ - Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.Kind().Group(), - Version: v0alpha1.Kind().Version(), - Resource: v0alpha1.Kind().Plural(), - }).Namespace("default"), - } -} diff --git a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go index 42b416d978d..695e63cfb4c 100644 --- a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go +++ b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go @@ -15,14 +15,14 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/dynamic" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1" "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1/fakes" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" @@ -34,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/tests/api/alerting" "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" @@ -57,7 +58,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - client := newClient(t, helper.Org1.Admin) + client := common.NewTimeIntervalClient(t, helper.Org1.Admin) newInterval := &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ @@ -191,11 +192,11 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) { }, } - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin) for _, tc := range testCases { t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { - client := newClient(t, tc.user) + client := common.NewTimeIntervalClient(t, tc.user) var expected = &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ Namespace: "default", @@ -349,7 +350,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) { org := helper.Org1 admin := org.Admin - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin) env := helper.GetEnv() ac := acimpl.ProvideAccessControl(env.FeatureToggles) @@ -401,7 +402,7 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin) interval := v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ @@ -485,7 +486,7 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin) interval := v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ @@ -550,7 +551,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin) interval1 := &v0alpha1.TimeInterval{ ObjectMeta: v1.ObjectMeta{ @@ -650,8 +651,26 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) { var amConfig definitions.PostableUserConfig require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig)) - success, err := legacyCli.PostConfiguration(t, amConfig) - require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err) + mtis := []definitions.MuteTimeInterval{} + for _, interval := range amConfig.AlertmanagerConfig.MuteTimeIntervals { + mtis = append(mtis, definitions.MuteTimeInterval{ + MuteTimeInterval: interval, + }) + } + + adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin) + v1intervals, err := timeinterval.ConvertToK8sResources(orgID, mtis, func(int64) string { return "default" }, nil) + require.NoError(t, err) + for _, interval := range v1intervals.Items { + _, err := adminClient.Create(ctx, &interval, v1.CreateOptions{}) + require.NoError(t, err) + } + + routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin) + v1route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" }) + require.NoError(t, err) + _, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{}) + require.NoError(t, err) postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json")) require.NoError(t, err) @@ -667,8 +686,6 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) { currentRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name) require.Equal(t, http.StatusAccepted, status) - adminClient := newClient(t, helper.Org1.Admin) - intervals, err := adminClient.List(ctx, v1.ListOptions{}) require.NoError(t, err) require.Len(t, intervals.Items, 2) @@ -769,7 +786,7 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) { ctx := context.Background() helper := getTestHelper(t) - adminClient := newClient(t, helper.Org1.Admin) + adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin) testCases := []struct { name string @@ -809,19 +826,3 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) { }) } } - -func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList] { - t.Helper() - - client, err := dynamic.NewForConfig(user.NewRestConfig()) - require.NoError(t, err) - - return &apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList]{ - Client: client.Resource( - schema.GroupVersionResource{ - Group: v0alpha1.Kind().Group(), - Version: v0alpha1.Kind().Version(), - Resource: v0alpha1.Kind().Plural(), - }).Namespace("default"), - } -} diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go index f6ffedfc601..a630d12bf34 100644 --- a/pkg/tests/apis/dashboard/dashboards_test.go +++ b/pkg/tests/apis/dashboard/dashboards_test.go @@ -42,7 +42,8 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper, gvr schema.Group obj := &unstructured.Unstructured{ Object: map[string]interface{}{ "spec": map[string]any{ - "title": "Test empty dashboard", + "title": "Test empty dashboard", + "schemaVersion": 41, }, }, } diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index 3551f7548f1..e5a82360482 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -7,7 +7,9 @@ import ( "strings" "testing" + dashboardv0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -212,26 +214,95 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { t.Run("reject dashboard with non-existent folder UID", func(t *testing.T) { nonExistentFolderUID := "non-existent-folder-uid" _, err := createDashboard(t, adminClient, "Dashboard in Non-existent Folder", &nonExistentFolderUID, nil) - ctx.Helper.EnsureStatusError(err, http.StatusNotFound, "folder not found") + ctx.Helper.EnsureStatusError(err, http.StatusNotFound, "folders.folder.grafana.app \"non-existent-folder-uid\" not found") }) }) t.Run("Dashboard schema validations", func(t *testing.T) { // Test invalid dashboard schema t.Run("reject dashboard with invalid schema", func(t *testing.T) { - dashObj := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, - "metadata": map[string]interface{}{ - "generateName": "test-", + testCases := []struct { + name string + resourceInfo utils.ResourceInfo + expectSpecErr bool + testObject *unstructured.Unstructured + }{ + { + name: "v0alpha1 dashboard with wrong spec should not throw on v0", + resourceInfo: dashboardv0alpha1.DashboardResourceInfo, + expectSpecErr: false, + testObject: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": dashboardv0alpha1.DashboardResourceInfo.TypeMeta().APIVersion, + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "generateName": "test-", + }, + "spec": map[string]interface{}{ + "title": "Dashboard Title", + "schemaVersion": 41, + "editable": "elephant", + "time": 9000, + "uid": strings.Repeat("a", 100), + }, + }, + }, + }, + { + name: "v1 dashboard with wrong spec should throw on v1", + resourceInfo: dashboardv1.DashboardResourceInfo, + expectSpecErr: true, + testObject: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": dashboardv1.DashboardResourceInfo.TypeMeta().APIVersion, + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "generateName": "test-", + }, + "spec": map[string]interface{}{ + "title": "Dashboard Title", + "schemaVersion": 41, + "editable": "elephant", + "time": 9000, + "uid": strings.Repeat("a", 100), + }, + }, + }, + }, + { + name: "v2alpha1 dashboard with correct spec should not throw on v2", + resourceInfo: dashboardv2alpha1.DashboardResourceInfo, + expectSpecErr: false, + testObject: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": dashboardv2alpha1.DashboardResourceInfo.TypeMeta().APIVersion, + "kind": "Dashboard", + "metadata": map[string]interface{}{ + "generateName": "test-", + }, + "spec": map[string]interface{}{ + "title": "Dashboard Title", + "description": "valid description", + }, + }, }, - // Missing spec }, } - _, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{}) - require.Error(t, err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resourceClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, tc.resourceInfo.GroupVersionResource()) + createdDashboard, err := resourceClient.Resource.Create(context.Background(), tc.testObject, v1.CreateOptions{}) + if tc.expectSpecErr { + ctx.Helper.RequireApiErrorStatus(err, v1.StatusReasonInvalid, http.StatusUnprocessableEntity) + } else { + require.NoError(t, err) + require.NotNil(t, createdDashboard) + err = resourceClient.Resource.Delete(context.Background(), createdDashboard.GetName(), v1.DeleteOptions{}) + require.NoError(t, err) + } + }) + } }) }) @@ -683,20 +754,12 @@ func createTestContext(t *testing.T, helper *apis.K8sTestHelper, orgUsers apis.O // getDashboardGVR returns the dashboard GroupVersionResource func getDashboardGVR() schema.GroupVersionResource { - return schema.GroupVersionResource{ - Group: dashboardv1.DashboardResourceInfo.GroupVersion().Group, - Version: dashboardv1.DashboardResourceInfo.GroupVersion().Version, - Resource: dashboardv1.DashboardResourceInfo.GetName(), - } + return dashboardv1.DashboardResourceInfo.GroupVersionResource() } // getFolderGVR returns the folder GroupVersionResource func getFolderGVR() schema.GroupVersionResource { - return schema.GroupVersionResource{ - Group: folders.FolderResourceInfo.GroupVersion().Group, - Version: folders.FolderResourceInfo.GroupVersion().Version, - Resource: folders.FolderResourceInfo.GetName(), - } + return folders.FolderResourceInfo.GroupVersionResource() } // Get a resource client for the specified user @@ -793,7 +856,8 @@ func createDashboardObject(t *testing.T, title string, folderUID string, generat }, }, "spec": map[string]interface{}{ - "title": title, + "title": title, + "schemaVersion": 41, }, }, } diff --git a/pkg/tests/apis/dashboard/testdata/dashboard-test-v1.yaml b/pkg/tests/apis/dashboard/testdata/dashboard-test-v1.yaml index 1e17651de60..6ad80f602c6 100644 --- a/pkg/tests/apis/dashboard/testdata/dashboard-test-v1.yaml +++ b/pkg/tests/apis/dashboard/testdata/dashboard-test-v1.yaml @@ -6,3 +6,4 @@ spec: title: Test dashboard. Created at v1 uid: test-v1 # will be removed by mutation hook version: 1234567 # will be removed by mutation hook + schemaVersion: 41 diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 2c2b7f414f5..62f1877586c 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -28,6 +28,7 @@ import ( "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/server" @@ -894,3 +895,22 @@ func (c *K8sTestHelper) DeleteServiceAccount(user User, orgID int64, saID int64) require.Equal(c.t, http.StatusOK, resp.Response.StatusCode, "failed to delete service account, body: %s", string(resp.Body)) } + +// Ensures that the passed error is an APIStatus error and fails the test if it is not. +func (c *K8sTestHelper) RequireApiErrorStatus(err error, reason metav1.StatusReason, httpCode int) metav1.Status { + require.Error(c.t, err) + status, ok := utils.ExtractApiErrorStatus(err) + if !ok { + c.t.Fatalf("Expected error to be an APIStatus, but got %T", err) + } + + if reason != metav1.StatusReasonUnknown { + require.Equal(c.t, status.Reason, reason) + } + + if httpCode != 0 { + require.Equal(c.t, status.Code, int32(httpCode)) + } + + return status +} diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 556cc089928..8892f4b21cc 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3294,7 +3294,6 @@ ], "properties": { "annotations": { - "description": "Title of dashboard.", "type": "array", "items": { "default": {}, diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index 45dd20e4530..82b2537f563 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -2472,6 +2472,24 @@ } } }, + "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ErrorDetails": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "detail": { + "type": "string" + }, + "field": { + "type": "string" + }, + "type": { + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ExportJobOptions": { "type": "object", "properties": { @@ -3762,20 +3780,16 @@ "format": "int32", "default": 0 }, - "details": { - "description": "Optional details", - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured" - } - ] - }, "errors": { - "description": "Error descriptions", + "description": "Field related errors", "type": "array", "items": { - "type": "string", - "default": "" + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.provisioning.v0alpha1.ErrorDetails" + } + ] } }, "kind": { diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index d59a01421ca..57ab7a1fabf 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -48,6 +48,7 @@ type provisioningTestHelper struct { Folders *apis.K8sResourceClient Dashboards *apis.K8sResourceClient AdminREST *rest.RESTClient + EditorREST *rest.RESTClient ViewerREST *rest.RESTClient } @@ -256,13 +257,10 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper }) // Repo client, but less guard rails. Useful for subresources. We'll need this later... - restClient := helper.Org1.Admin.RESTClient(t, &schema.GroupVersion{ - Group: "provisioning.grafana.app", Version: "v0alpha1", - }) - - viewerClient := helper.Org1.Viewer.RESTClient(t, &schema.GroupVersion{ - Group: "provisioning.grafana.app", Version: "v0alpha1", - }) + gv := &schema.GroupVersion{Group: "provisioning.grafana.app", Version: "v0alpha1"} + adminClient := helper.Org1.Admin.RESTClient(t, gv) + editorClient := helper.Org1.Editor.RESTClient(t, gv) + viewerClient := helper.Org1.Viewer.RESTClient(t, gv) deleteAll := func(client *apis.K8sResourceClient) error { ctx := context.Background() @@ -287,7 +285,8 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper K8sTestHelper: helper, Repositories: repositories, - AdminREST: restClient, + AdminREST: adminClient, + EditorREST: editorClient, ViewerREST: viewerClient, Jobs: jobs, Folders: folders, diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index 6b531807381..b346ed58cc5 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -9,6 +9,7 @@ import ( "regexp" "strings" "testing" + "time" gh "github.com/google/go-github/v70/github" ghmock "github.com/migueleliasweb/go-github-mock/src/mock" @@ -23,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/tests/apis" + "k8s.io/apimachinery/pkg/runtime" ) func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) { @@ -119,6 +121,88 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) { }) } +func TestIntegrationProvisioning_FailInvalidSchema(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + t.Skip("Reenable this test once we enforce schema validation for provisioning") + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "invalid-schema-tmp" + // Set up the repository and the file to import. + helper.CopyToProvisioningPath(t, "testdata/invalid-dashboard-schema.json", "invalid-dashboard-schema.json") + + localTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repo, + "SyncEnabled": true, + }) + _, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{}) + require.NoError(t, err) + + // Make sure the repo can read and validate the file + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "invalid-dashboard-schema.json") + status := helper.RequireApiErrorStatus(err, metav1.StatusReasonBadRequest, http.StatusBadRequest) + require.Equal(t, status.Message, "Dry run failed: Dashboard.dashboard.grafana.app \"invalid-schema-uid\" is invalid: [spec.panels.0.repeatDirection: Invalid value: conflicting values \"h\" and \"this is not an allowed value\", spec.panels.0.repeatDirection: Invalid value: conflicting values \"v\" and \"this is not an allowed value\"]") + + const invalidSchemaUid = "invalid-schema-uid" + _, err = helper.Dashboards.Resource.Get(ctx, invalidSchemaUid, metav1.GetOptions{}) + require.Error(t, err, "invalid dashboard shouldn't exist") + require.True(t, apierrors.IsNotFound(err)) + + var jobObj *unstructured.Unstructured + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(asJSON(&provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{}, + })). + SetHeader("Content-Type", "application/json"). + Do(t.Context()) + require.NoError(collect, result.Error()) + job, err := result.Get() + require.NoError(collect, err) + var ok bool + jobObj, ok = job.(*unstructured.Unstructured) + require.True(collect, ok, "expecting unstructured object, but got %T", job) + }, time.Second*10, time.Millisecond*10, "Expected to be able to start a sync job") + + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + //helper.TriggerJobProcessing(t) + result, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, + "jobs", string(jobObj.GetUID())) + + if apierrors.IsNotFound(err) { + assert.Fail(collect, "job '%s' not found yet yet", jobObj.GetName()) + return // continue trying + } + + // Can fail fast here -- the jobs are immutable + require.NoError(t, err) + require.NotNil(t, result) + + job := &provisioning.Job{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.Object, job) + require.NoError(t, err, "should convert to Job object") + + require.Equal(t, provisioning.JobStateError, job.Status.State) + require.Equal(t, job.Status.Message, "completed with errors") + require.Equal(t, job.Status.Errors[0], "Dashboard.dashboard.grafana.app \"invalid-schema-uid\" is invalid: [spec.panels.0.repeatDirection: Invalid value: conflicting values \"h\" and \"this is not an allowed value\", spec.panels.0.repeatDirection: Invalid value: conflicting values \"v\" and \"this is not an allowed value\"]") + }, time.Second*10, time.Millisecond*10, "Expected provisioning job to conclude with the status failed") + + _, err = helper.Dashboards.Resource.Get(ctx, invalidSchemaUid, metav1.GetOptions{}) + require.Error(t, err, "invalid dashboard shouldn't have been created") + require.True(t, apierrors.IsNotFound(err)) + + err = helper.Repositories.Resource.Delete(ctx, repo, metav1.DeleteOptions{}, "files", "invalid-dashboard-schema.json") + require.NoError(t, err, "should delete the resource file") +} + func TestIntegrationProvisioning_CreatingGitHubRepository(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -283,8 +367,8 @@ func TestIntegrationProvisioning_RunLocalRepository(t *testing.T) { require.Equal(t, http.StatusNotFound, code) require.True(t, apierrors.IsNotFound(result.Error())) - // Now try again with POST - result = helper.AdminREST.Post(). + // Now try again with POST (as an editor) + result = helper.EditorREST.Post(). Namespace("default"). Resource("repositories"). Name(repo). diff --git a/pkg/tests/apis/provisioning/testdata/invalid-dashboard-schema.json b/pkg/tests/apis/provisioning/testdata/invalid-dashboard-schema.json new file mode 100644 index 00000000000..8038506ab0f --- /dev/null +++ b/pkg/tests/apis/provisioning/testdata/invalid-dashboard-schema.json @@ -0,0 +1,29 @@ +{ + "title": "Provisioning test - invalid schema", + "uid": "invalid-schema-uid", + "schemaVersion": 41, + "tags": [ + "tag" + ], + "revision": "this-is-not-a-number", + "panels": [ + { + "gridPos": { + "h": 3, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 34, + "options": { + "content": "# All panels\n\nThis dashboard was created to quickly check accessiblity issues on a lot of panels at the same time ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "transparent": true, + "type": "text", + "repeat": "something", + "repeatDirection": "this is not an allowed value" + } + ] +} diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index 868e566ddf8..746e670dc78 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -162,7 +162,11 @@ func (c *baseClientImpl) executeRequest(method, uriPath, uriQuery string, body [ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearchResponse, error) { var err error - multiRequests := c.createMultiSearchRequests(r.Requests) + multiRequests, err := c.createMultiSearchRequests(r.Requests) + if err != nil { + return nil, err + } + queryParams := c.getMultiSearchQueryParameters() _, span := tracing.DefaultTracer().Start(c.ctx, "datasource.elasticsearch.queryData.executeMultisearch", trace.WithAttributes( attribute.String("queryParams", queryParams), @@ -429,14 +433,14 @@ func skipUnknownField(dec *json.Decoder) error { } } -func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchRequest) []*multiRequest { +func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchRequest) ([]*multiRequest, error) { multiRequests := []*multiRequest{} for _, searchReq := range searchRequests { indices, err := c.indexPattern.GetIndices(searchReq.TimeRange) if err != nil { - c.logger.Error("Failed to get indices from index pattern", "error", err) - continue + err := fmt.Errorf("failed to get indices from index pattern. %s", err) + return nil, backend.DownstreamError(err) } mr := multiRequest{ header: map[string]any{ @@ -451,7 +455,7 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque multiRequests = append(multiRequests, &mr) } - return multiRequests + return multiRequests, nil } func (c *baseClientImpl) getMultiSearchQueryParameters() string { diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go index dff8b39139d..8f257873232 100644 --- a/pkg/tsdb/elasticsearch/client/client_test.go +++ b/pkg/tsdb/elasticsearch/client/client_test.go @@ -183,6 +183,21 @@ func TestClient_ExecuteMultisearch(t *testing.T) { require.Contains(t, bodyString, "metrics-2018.05.17") }) + t.Run("Should return DownstreamError when index is invalid", func(t *testing.T) { + ds := &DatasourceInfo{ + URL: "test", + Database: "index-with-no-pattern", + Interval: intervalMonthly, + } + + c, err := NewClient(context.Background(), ds, log.NewNullLogger()) + require.NoError(t, err) + + _, err = c.ExecuteMultisearch(&MultiSearchRequest{Requests: []*SearchRequest{{}}}) + assert.Equal(t, "failed to get indices from index pattern. invalid index pattern index-with-no-pattern. Specify an index with a time pattern or select 'No pattern'", err.Error()) + require.True(t, backend.IsDownstreamError(err)) + }) + t.Run("Should return DownstreamError when decoding response fails", func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/x-ndjson") diff --git a/pkg/tsdb/elasticsearch/client/index_pattern.go b/pkg/tsdb/elasticsearch/client/index_pattern.go index ff01d759c5c..06da9004678 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern.go @@ -136,7 +136,12 @@ func (ip *dynamicIndexPattern) GetIndices(timeRange backend.TimeRange) ([]string indices := make([]string, 0) for _, t := range intervals { - indices = append(indices, formatDate(t, ip.pattern)) + index, err := formatDate(t, ip.pattern) + if err != nil { + return []string{}, err + } else { + indices = append(indices, index) + } } return indices, nil @@ -251,13 +256,16 @@ func (i *yearlyInterval) Generate(from, to time.Time) []time.Time { return intervals } -func formatDate(t time.Time, pattern string) string { +func formatDate(t time.Time, pattern string) (string, error) { var formattedDatePatterns []string var bases []string base := "" isBaseFirst := false baseStart := strings.Index(pattern, "[") + if baseStart == -1 { + return "", fmt.Errorf("invalid index pattern %s. Specify an index with a time pattern or select 'No pattern'", pattern) + } for baseStart != -1 { var datePattern string @@ -344,7 +352,7 @@ func formatDate(t time.Time, pattern string) string { fullPattern = append(fullPattern, bases...) } - return strings.Join(fullPattern, "") + return strings.Join(fullPattern, ""), nil } func patternToLayout(pattern string) string { diff --git a/pkg/tsdb/elasticsearch/client/index_pattern_test.go b/pkg/tsdb/elasticsearch/client/index_pattern_test.go index a30408cf4a8..7468fc4580e 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern_test.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -108,6 +109,21 @@ func TestIndexPattern(t *testing.T) { }) }) + t.Run("Dynamic index pattern with error", func(t *testing.T) { + from := time.Date(2018, 5, 15, 17, 50, 0, 0, time.UTC) + to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC) + timeRange := backend.TimeRange{ + From: from, + To: to, + } + ip, err := NewIndexPattern(intervalHourly, "kibana-sample-data-logs") + require.NoError(t, err) + indices, err := ip.GetIndices(timeRange) + assert.Equal(t, indices, []string{}) + require.Error(t, err) + assert.Equal(t, err.Error(), "invalid index pattern kibana-sample-data-logs. Specify an index with a time pattern or select 'No pattern'") + }) + t.Run("Hourly interval", func(t *testing.T) { t.Run("Should return 1 interval", func(t *testing.T) { from := time.Date(2018, 1, 1, 23, 1, 1, 0, time.UTC) diff --git a/pkg/tsdb/influxdb/fsql/fsql.go b/pkg/tsdb/influxdb/fsql/fsql.go index 90925031f57..6869696fab3 100644 --- a/pkg/tsdb/influxdb/fsql/fsql.go +++ b/pkg/tsdb/influxdb/fsql/fsql.go @@ -3,6 +3,7 @@ package fsql import ( "context" "fmt" + "net/http" "net/url" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -62,6 +63,8 @@ func Query(ctx context.Context, dsInfo *models.DatasourceInfo, req backend.Query tRes.Responses[q.RefID] = backend.ErrDataResponseWithSource(backend.StatusForbidden, backend.ErrorSourceDownstream, errStr) case codes.NotFound: tRes.Responses[q.RefID] = backend.ErrDataResponseWithSource(backend.StatusNotFound, backend.ErrorSourceDownstream, errStr) + case codes.Unavailable: + tRes.Responses[q.RefID] = backend.ErrDataResponseWithSource(http.StatusServiceUnavailable, backend.ErrorSourceDownstream, errStr) default: tRes.Responses[q.RefID] = backend.ErrDataResponse(backend.StatusInternal, errStr) } diff --git a/pkg/tsdb/jaeger/client.go b/pkg/tsdb/jaeger/client.go index 3e5ca5f5daf..79bef83fac4 100644 --- a/pkg/tsdb/jaeger/client.go +++ b/pkg/tsdb/jaeger/client.go @@ -26,6 +26,20 @@ type ServicesResponse struct { Total int `json:"total"` } +type DependenciesResponse struct { + Data []ServiceDependency `json:"data"` + Errors []struct { + Code int `json:"code"` + Msg string `json:"msg"` + } `json:"errors"` +} + +type ServiceDependency struct { + Parent string `json:"parent"` + Child string `json:"child"` + CallCount int `json:"callCount"` +} + func New(url string, hc *http.Client, logger log.Logger, traceIdTimeEnabled bool) (JaegerClient, error) { client := JaegerClient{ logger: logger, @@ -158,3 +172,59 @@ func (j *JaegerClient) Trace(ctx context.Context, traceID string, start, end int trace = response.Data[0] return trace, err } + +func (j *JaegerClient) Dependencies(ctx context.Context, start, end int64) (DependenciesResponse, error) { + logger := j.logger.FromContext(ctx) + var dependencies DependenciesResponse + + u, err := url.JoinPath(j.url, "/api/dependencies") + if err != nil { + return dependencies, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) + } + + // Add time parameters + parsedURL, err := url.Parse(u) + if err != nil { + return dependencies, backend.DownstreamError(fmt.Errorf("failed to parse url: %w", err)) + } + + query := parsedURL.Query() + if end > 0 { + query.Set("endTs", fmt.Sprintf("%d", end)) + } + if start > 0 { + lookback := end - start + query.Set("lookback", fmt.Sprintf("%d", lookback)) + } + + parsedURL.RawQuery = query.Encode() + u = parsedURL.String() + + res, err := j.httpClient.Get(u) + if err != nil { + if backend.IsDownstreamHTTPError(err) { + return dependencies, backend.DownstreamError(err) + } + return dependencies, err + } + + defer func() { + if err = res.Body.Close(); err != nil { + logger.Error("Failed to close response body", "error", err) + } + }() + + if res != nil && res.StatusCode/100 != 2 { + err := backend.DownstreamError(fmt.Errorf("request failed: %s", res.Status)) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return dependencies, backend.DownstreamError(err) + } + return dependencies, err + } + + if err := json.NewDecoder(res.Body).Decode(&dependencies); err != nil { + return dependencies, err + } + + return dependencies, nil +} diff --git a/pkg/tsdb/jaeger/client_test.go b/pkg/tsdb/jaeger/client_test.go index fc34fa61379..67a83d84668 100644 --- a/pkg/tsdb/jaeger/client_test.go +++ b/pkg/tsdb/jaeger/client_test.go @@ -276,3 +276,112 @@ func TestJaegerClient_Trace(t *testing.T) { }) } } + +func TestJaegerClient_Dependencies(t *testing.T) { + tests := []struct { + name string + start int64 + end int64 + mockResponse string + mockStatusCode int + mockStatus string + expectedURL string + expectError bool + expectedError error + }{ + { + name: "Successful response with time range", + start: 1000, + end: 2000, + mockResponse: `{"data":[{"parent":"serviceA","child":"serviceB","callCount":1}]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/dependencies?endTs=2000&lookback=1000", + expectError: false, + expectedError: nil, + }, + { + name: "Successful response without time range", + start: 0, + end: 0, + mockResponse: `{"data":[{"parent":"serviceA","child":"serviceB","callCount":1}]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/dependencies", + expectError: false, + expectedError: nil, + }, + { + name: "Non-200 response", + start: 1000, + end: 2000, + mockResponse: "", + mockStatusCode: http.StatusInternalServerError, + mockStatus: "Internal Server Error", + expectedURL: "/api/dependencies?endTs=2000&lookback=1000", + expectError: true, + expectedError: backend.PluginError(errors.New("Internal Server Error")), + }, + { + name: "Invalid JSON response", + start: 1000, + end: 2000, + mockResponse: `{invalid json`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/dependencies?endTs=2000&lookback=1000", + expectError: true, + expectedError: &json.SyntaxError{}, + }, + { + name: "Empty dependencies response and no errors", + start: 1000, + end: 2000, + mockResponse: `{"data":[]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/dependencies?endTs=2000&lookback=1000", + expectError: false, + expectedError: nil, + }, + { + name: "Response with errors", + start: 1000, + end: 2000, + mockResponse: `{"data":[],"errors":[{"code":500,"msg":"Internal error"}]}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedURL: "/api/dependencies?endTs=2000&lookback=1000", + expectError: false, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actualURL string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + actualURL = r.URL.String() + w.WriteHeader(tt.mockStatusCode) + _, _ = w.Write([]byte(tt.mockResponse)) + })) + defer server.Close() + + client, err := New(server.URL, server.Client(), log.NewNullLogger(), false) + assert.NoError(t, err) + + dependencies, err := client.Dependencies(context.Background(), tt.start, tt.end) + + if tt.expectError { + assert.Error(t, err) + if tt.expectedError != nil { + assert.IsType(t, tt.expectedError, err) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, dependencies) + } + assert.Equal(t, tt.expectedURL, actualURL) + }) + } +} diff --git a/pkg/tsdb/jaeger/querydata.go b/pkg/tsdb/jaeger/querydata.go index 85aed936bff..1f2202d6cf0 100644 --- a/pkg/tsdb/jaeger/querydata.go +++ b/pkg/tsdb/jaeger/querydata.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -45,6 +46,23 @@ func queryData(ctx context.Context, dsInfo *datasourceInfo, req *backend.QueryDa Frames: []*data.Frame{frame}, } } + + if query.QueryType == "dependencyGraph" { + dependencies, err := dsInfo.JaegerClient.Dependencies(ctx, q.TimeRange.From.UnixMilli(), q.TimeRange.To.UnixMilli()) + if err != nil { + response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(err) + continue + } + + if len(dependencies.Errors) > 0 { + response.Responses[q.RefID] = backend.ErrorResponseWithErrorSource(backend.DownstreamError(fmt.Errorf("error while fetching dependencies, code: %v, message: %v", dependencies.Errors[0].Code, dependencies.Errors[0].Msg))) + continue + } + frames := transformDependenciesResponse(dependencies, q.RefID) + response.Responses[q.RefID] = backend.DataResponse{ + Frames: frames, + } + } } return response, nil @@ -215,3 +233,70 @@ type TracesResponse struct { Offset int `json:"offset"` Total int `json:"total"` } + +func transformDependenciesResponse(dependencies DependenciesResponse, refID string) []*data.Frame { + // Create nodes frame + nodesFrame := data.NewFrame(refID+"_nodes", + data.NewField("id", nil, []string{}), + data.NewField("title", nil, []string{}), + ) + nodesFrame.Meta = &data.FrameMeta{ + PreferredVisualization: "nodeGraph", + } + + // Create edges frame + mainStatField := data.NewField("mainstat", nil, []int64{}) + mainStatField.Config = &data.FieldConfig{ + DisplayName: "Call count", + } + edgesFrame := data.NewFrame(refID+"_edges", + data.NewField("id", nil, []string{}), + data.NewField("source", nil, []string{}), + data.NewField("target", nil, []string{}), + mainStatField, + ) + + edgesFrame.Meta = &data.FrameMeta{ + PreferredVisualization: "nodeGraph", + } + + // Return early if there are no dependencies + if len(dependencies.Data) == 0 { + return []*data.Frame{nodesFrame, edgesFrame} + } + + // Create a map to store unique service nodes + servicesByName := make(map[string]bool) + + // Process each dependency + for _, dependency := range dependencies.Data { + // Add services to the map to track unique services + servicesByName[dependency.Parent] = true + servicesByName[dependency.Child] = true + + // Add edge data + edgesFrame.AppendRow( + dependency.Parent+"--"+dependency.Child, + dependency.Parent, + dependency.Child, + int64(dependency.CallCount), + ) + } + + // Convert map keys to slice and sort them - this is to ensure the returned nodes are in a consistent order + services := make([]string, 0, len(servicesByName)) + for service := range servicesByName { + services = append(services, service) + } + sort.Strings(services) + + // Add node data in sorted order + for _, service := range services { + nodesFrame.AppendRow( + service, + service, + ) + } + + return []*data.Frame{nodesFrame, edgesFrame} +} diff --git a/pkg/tsdb/jaeger/querydata_test.go b/pkg/tsdb/jaeger/querydata_test.go index 8dc6dc3b0ad..7dbe86041ea 100644 --- a/pkg/tsdb/jaeger/querydata_test.go +++ b/pkg/tsdb/jaeger/querydata_test.go @@ -184,3 +184,97 @@ func TestTransformTraceResponse(t *testing.T) { experimental.CheckGoldenJSONFrame(t, "./testdata", "complex_trace.golden", frame, false) }) } + +func TestTransformDependenciesResponse(t *testing.T) { + t.Run("simple_dependencies", func(t *testing.T) { + dependencies := DependenciesResponse{ + Data: []ServiceDependency{ + { + Parent: "serviceA", + Child: "serviceB", + CallCount: 1, + }, + { + Parent: "serviceA", + Child: "serviceC", + CallCount: 2, + }, + { + Parent: "serviceB", + Child: "serviceC", + CallCount: 3, + }, + }, + } + + frames := transformDependenciesResponse(dependencies, "test") + experimental.CheckGoldenJSONFrame(t, "./testdata", "simple_dependencies_nodes.golden", frames[0], false) + experimental.CheckGoldenJSONFrame(t, "./testdata", "simple_dependencies_edges.golden", frames[1], false) + }) + + t.Run("empty_dependencies", func(t *testing.T) { + dependencies := DependenciesResponse{ + Data: []ServiceDependency{}, + } + + frames := transformDependenciesResponse(dependencies, "test") + experimental.CheckGoldenJSONFrame(t, "./testdata", "empty_dependencies_nodes.golden", frames[0], false) + experimental.CheckGoldenJSONFrame(t, "./testdata", "empty_dependencies_edges.golden", frames[1], false) + }) + + t.Run("complex_dependencies", func(t *testing.T) { + dependencies := DependenciesResponse{ + Data: []ServiceDependency{ + { + Parent: "frontend", + Child: "auth-service", + CallCount: 150, + }, + { + Parent: "frontend", + Child: "api-gateway", + CallCount: 300, + }, + { + Parent: "api-gateway", + Child: "user-service", + CallCount: 200, + }, + { + Parent: "api-gateway", + Child: "order-service", + CallCount: 100, + }, + { + Parent: "order-service", + Child: "payment-service", + CallCount: 80, + }, + { + Parent: "order-service", + Child: "inventory-service", + CallCount: 90, + }, + { + Parent: "user-service", + Child: "database", + CallCount: 500, + }, + { + Parent: "payment-service", + Child: "database", + CallCount: 200, + }, + { + Parent: "inventory-service", + Child: "database", + CallCount: 300, + }, + }, + } + + frames := transformDependenciesResponse(dependencies, "test") + experimental.CheckGoldenJSONFrame(t, "./testdata", "complex_dependencies_nodes.golden", frames[0], false) + experimental.CheckGoldenJSONFrame(t, "./testdata", "complex_dependencies_edges.golden", frames[1], false) + }) +} diff --git a/pkg/tsdb/jaeger/testdata/complex_dependencies_edges.golden.jsonc b/pkg/tsdb/jaeger/testdata/complex_dependencies_edges.golden.jsonc new file mode 100644 index 00000000000..df3abb4f180 --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/complex_dependencies_edges.golden.jsonc @@ -0,0 +1,127 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "preferredVisualisationType": "nodeGraph" +// } +// Name: test_edges +// Dimensions: 4 Fields by 9 Rows +// +----------------------------------+-------------------+-------------------+----------------+ +// | Name: id | Name: source | Name: target | Name: mainstat | +// | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []string | Type: []int64 | +// +----------------------------------+-------------------+-------------------+----------------+ +// | frontend--auth-service | frontend | auth-service | 150 | +// | frontend--api-gateway | frontend | api-gateway | 300 | +// | api-gateway--user-service | api-gateway | user-service | 200 | +// | api-gateway--order-service | api-gateway | order-service | 100 | +// | order-service--payment-service | order-service | payment-service | 80 | +// | order-service--inventory-service | order-service | inventory-service | 90 | +// | user-service--database | user-service | database | 500 | +// | payment-service--database | payment-service | database | 200 | +// | inventory-service--database | inventory-service | database | 300 | +// +----------------------------------+-------------------+-------------------+----------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test_edges", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "preferredVisualisationType": "nodeGraph" + }, + "fields": [ + { + "name": "id", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "source", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "target", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "mainstat", + "type": "number", + "typeInfo": { + "frame": "int64" + }, + "config": { + "displayName": "Call count" + } + } + ] + }, + "data": { + "values": [ + [ + "frontend--auth-service", + "frontend--api-gateway", + "api-gateway--user-service", + "api-gateway--order-service", + "order-service--payment-service", + "order-service--inventory-service", + "user-service--database", + "payment-service--database", + "inventory-service--database" + ], + [ + "frontend", + "frontend", + "api-gateway", + "api-gateway", + "order-service", + "order-service", + "user-service", + "payment-service", + "inventory-service" + ], + [ + "auth-service", + "api-gateway", + "user-service", + "order-service", + "payment-service", + "inventory-service", + "database", + "database", + "database" + ], + [ + 150, + 300, + 200, + 100, + 80, + 90, + 500, + 200, + 300 + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/testdata/complex_dependencies_nodes.golden.jsonc b/pkg/tsdb/jaeger/testdata/complex_dependencies_nodes.golden.jsonc new file mode 100644 index 00000000000..f20e91ddba6 --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/complex_dependencies_nodes.golden.jsonc @@ -0,0 +1,85 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "preferredVisualisationType": "nodeGraph" +// } +// Name: test_nodes +// Dimensions: 2 Fields by 8 Rows +// +-------------------+-------------------+ +// | Name: id | Name: title | +// | Labels: | Labels: | +// | Type: []string | Type: []string | +// +-------------------+-------------------+ +// | api-gateway | api-gateway | +// | auth-service | auth-service | +// | database | database | +// | frontend | frontend | +// | inventory-service | inventory-service | +// | order-service | order-service | +// | payment-service | payment-service | +// | user-service | user-service | +// +-------------------+-------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test_nodes", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "preferredVisualisationType": "nodeGraph" + }, + "fields": [ + { + "name": "id", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "title", + "type": "string", + "typeInfo": { + "frame": "string" + } + } + ] + }, + "data": { + "values": [ + [ + "api-gateway", + "auth-service", + "database", + "frontend", + "inventory-service", + "order-service", + "payment-service", + "user-service" + ], + [ + "api-gateway", + "auth-service", + "database", + "frontend", + "inventory-service", + "order-service", + "payment-service", + "user-service" + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/testdata/empty_dependencies_edges.golden.jsonc b/pkg/tsdb/jaeger/testdata/empty_dependencies_edges.golden.jsonc new file mode 100644 index 00000000000..cdfca484c20 --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/empty_dependencies_edges.golden.jsonc @@ -0,0 +1,78 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "preferredVisualisationType": "nodeGraph" +// } +// Name: test_edges +// Dimensions: 4 Fields by 0 Rows +// +----------------+----------------+----------------+----------------+ +// | Name: id | Name: source | Name: target | Name: mainstat | +// | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []string | Type: []int64 | +// +----------------+----------------+----------------+----------------+ +// +----------------+----------------+----------------+----------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test_edges", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "preferredVisualisationType": "nodeGraph" + }, + "fields": [ + { + "name": "id", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "source", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "target", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "mainstat", + "type": "number", + "typeInfo": { + "frame": "int64" + }, + "config": { + "displayName": "Call count" + } + } + ] + }, + "data": { + "values": [ + [], + [], + [], + [] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/testdata/empty_dependencies_nodes.golden.jsonc b/pkg/tsdb/jaeger/testdata/empty_dependencies_nodes.golden.jsonc new file mode 100644 index 00000000000..ba5e5512620 --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/empty_dependencies_nodes.golden.jsonc @@ -0,0 +1,59 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "preferredVisualisationType": "nodeGraph" +// } +// Name: test_nodes +// Dimensions: 2 Fields by 0 Rows +// +----------------+----------------+ +// | Name: id | Name: title | +// | Labels: | Labels: | +// | Type: []string | Type: []string | +// +----------------+----------------+ +// +----------------+----------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test_nodes", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "preferredVisualisationType": "nodeGraph" + }, + "fields": [ + { + "name": "id", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "title", + "type": "string", + "typeInfo": { + "frame": "string" + } + } + ] + }, + "data": { + "values": [ + [], + [] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/testdata/simple_dependencies_edges.golden.jsonc b/pkg/tsdb/jaeger/testdata/simple_dependencies_edges.golden.jsonc new file mode 100644 index 00000000000..37a0845274f --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/simple_dependencies_edges.golden.jsonc @@ -0,0 +1,97 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "preferredVisualisationType": "nodeGraph" +// } +// Name: test_edges +// Dimensions: 4 Fields by 3 Rows +// +--------------------+----------------+----------------+----------------+ +// | Name: id | Name: source | Name: target | Name: mainstat | +// | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []string | Type: []int64 | +// +--------------------+----------------+----------------+----------------+ +// | serviceA--serviceB | serviceA | serviceB | 1 | +// | serviceA--serviceC | serviceA | serviceC | 2 | +// | serviceB--serviceC | serviceB | serviceC | 3 | +// +--------------------+----------------+----------------+----------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test_edges", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "preferredVisualisationType": "nodeGraph" + }, + "fields": [ + { + "name": "id", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "source", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "target", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "mainstat", + "type": "number", + "typeInfo": { + "frame": "int64" + }, + "config": { + "displayName": "Call count" + } + } + ] + }, + "data": { + "values": [ + [ + "serviceA--serviceB", + "serviceA--serviceC", + "serviceB--serviceC" + ], + [ + "serviceA", + "serviceA", + "serviceB" + ], + [ + "serviceB", + "serviceC", + "serviceC" + ], + [ + 1, + 2, + 3 + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/jaeger/testdata/simple_dependencies_nodes.golden.jsonc b/pkg/tsdb/jaeger/testdata/simple_dependencies_nodes.golden.jsonc new file mode 100644 index 00000000000..56bb35cdd5d --- /dev/null +++ b/pkg/tsdb/jaeger/testdata/simple_dependencies_nodes.golden.jsonc @@ -0,0 +1,70 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "typeVersion": [ +// 0, +// 0 +// ], +// "preferredVisualisationType": "nodeGraph" +// } +// Name: test_nodes +// Dimensions: 2 Fields by 3 Rows +// +----------------+----------------+ +// | Name: id | Name: title | +// | Labels: | Labels: | +// | Type: []string | Type: []string | +// +----------------+----------------+ +// | serviceA | serviceA | +// | serviceB | serviceB | +// | serviceC | serviceC | +// +----------------+----------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "status": 200, + "frames": [ + { + "schema": { + "name": "test_nodes", + "meta": { + "typeVersion": [ + 0, + 0 + ], + "preferredVisualisationType": "nodeGraph" + }, + "fields": [ + { + "name": "id", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "title", + "type": "string", + "typeInfo": { + "frame": "string" + } + } + ] + }, + "data": { + "values": [ + [ + "serviceA", + "serviceB", + "serviceC" + ], + [ + "serviceA", + "serviceB", + "serviceC" + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/util/debouncer/debouncer.go b/pkg/util/debouncer/debouncer.go index a6c0e7b22e3..df2a58b8020 100644 --- a/pkg/util/debouncer/debouncer.go +++ b/pkg/util/debouncer/debouncer.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/benbjohnson/clock" "github.com/grafana/dskit/instrument" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -91,6 +92,9 @@ type DebouncerOpts[T comparable] struct { // for the same key keep arriving, we guarantee processing after MaxWait from the first event. MaxWait time.Duration Reg prometheus.Registerer + + // clock can be used for testing to not having to relay on wall clock time. + clock clock.Clock } type Group[T comparable] struct { @@ -108,6 +112,8 @@ type Group[T comparable] struct { minWait time.Duration maxWait time.Duration metrics *metrics + + clock clock.Clock } // NewGroup creates a new debouncer group for processing events with unique keys. @@ -165,6 +171,10 @@ func NewGroup[T comparable](opts DebouncerOpts[T]) (*Group[T], error) { opts.ErrorHandler = func(_ T, _ error) {} } + if opts.clock == nil { + opts.clock = clock.New() + } + return &Group[T]{ buffer: make(chan T, opts.BufferSize), debouncers: make(map[T]*debouncer[T]), @@ -173,6 +183,7 @@ func NewGroup[T comparable](opts DebouncerOpts[T]) (*Group[T], error) { minWait: opts.MinWait, maxWait: opts.MaxWait, metrics: newMetrics(opts.Reg, opts.Name), + clock: opts.clock, }, nil } @@ -217,7 +228,7 @@ func (g *Group[T]) processValue(key T) { g.debouncersMu.Lock() deb, ok := g.debouncers[key] if !ok { - deb = newDebouncer[T](g.minWait, g.maxWait, key, func(v T) { + deb = newDebouncer[T](g.minWait, g.maxWait, g.clock, key, func(v T) { g.processWithMetrics(g.ctx, v, g.processHandler) g.debouncersMu.Lock() @@ -256,16 +267,18 @@ type debouncer[T comparable] struct { minWait time.Duration maxWait time.Duration processFunc func(T) + clock clock.Clock } // newDebouncer creates a new key debouncer. -func newDebouncer[T comparable](minWait, maxWait time.Duration, key T, processFunc func(T)) *debouncer[T] { +func newDebouncer[T comparable](minWait, maxWait time.Duration, clock clock.Clock, key T, processFunc func(T)) *debouncer[T] { deb := &debouncer[T]{ key: key, resetChan: make(chan struct{}, 1), minWait: minWait, maxWait: maxWait, processFunc: processFunc, + clock: clock, } return deb } @@ -285,8 +298,8 @@ func (d *debouncer[T]) reset() { // run manages the debouncing process for a specific key. func (d *debouncer[T]) run(ctx context.Context) { // Create timers after getting the first updateChan. - minTimer := time.NewTimer(d.minWait) - maxTimer := time.NewTimer(d.maxWait) + minTimer := d.clock.Timer(d.minWait) + maxTimer := d.clock.Timer(d.maxWait) defer func() { minTimer.Stop() maxTimer.Stop() diff --git a/pkg/util/debouncer/debouncer_test.go b/pkg/util/debouncer/debouncer_test.go index 711c8761836..8fd58efddc9 100644 --- a/pkg/util/debouncer/debouncer_test.go +++ b/pkg/util/debouncer/debouncer_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/benbjohnson/clock" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" @@ -53,6 +54,7 @@ func TestDebouncer(t *testing.T) { t.Run("should process values after max wait", func(t *testing.T) { processed := make(map[string]int, 1) + clockMock := clock.NewMock() group, err := NewGroup(DebouncerOpts[string]{ BufferSize: 10, @@ -62,6 +64,7 @@ func TestDebouncer(t *testing.T) { }, MinWait: 50 * time.Millisecond, MaxWait: 500 * time.Millisecond, + clock: clockMock, }) require.NoError(t, err) @@ -70,20 +73,17 @@ func TestDebouncer(t *testing.T) { group.Start(ctx) - ticker := time.NewTicker(time.Millisecond * 40) - defer ticker.Stop() - - start := time.Now() + start := clockMock.Now() for counter := 0; counter < 25; counter++ { - <-ticker.C _ = group.Add("key1") + clockMock.Add(time.Millisecond * 40) if processed["key1"] == 1 { break } } - require.WithinDuration(t, start.Add(time.Millisecond*500), time.Now(), time.Millisecond*100) + require.WithinDuration(t, start.Add(time.Millisecond*500), clockMock.Now(), time.Millisecond*100) }) t.Run("should handle buffer full", func(t *testing.T) { diff --git a/pkg/util/xorm/dialect_spanner.go b/pkg/util/xorm/dialect_spanner.go index 0f18fb2e6f7..87c00a3447e 100644 --- a/pkg/util/xorm/dialect_spanner.go +++ b/pkg/util/xorm/dialect_spanner.go @@ -383,7 +383,7 @@ func (s *spanner) CreateSequenceGenerator(db *sql.DB) (SequenceGenerator, error) return nil, err } - if connectorConfig.Params["inMemSequenceGenerator"] == "true" { + if connectorConfig.Params[strings.ToLower("inMemSequenceGenerator")] == "true" { // Switch to using in-memory sequence number generator. // Using database-based sequence generator doesn't work with emulator, as emulator // only supports single transaction. If there is already another transaction started diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index 2c959056a74..b0ebd23c17b 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -7,7 +7,7 @@ require ( github.com/googleapis/go-sql-spanner v1.11.1 github.com/mattn/go-sqlite3 v1.14.22 github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.71.0 + google.golang.org/grpc v1.71.1 xorm.io/builder v0.3.6 xorm.io/core v0.7.3 ) @@ -43,20 +43,20 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect google.golang.org/api v0.223.0 // indirect google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect diff --git a/pkg/util/xorm/go.sum b/pkg/util/xorm/go.sum index fc04f60d31d..a2dc6d7ccda 100644 --- a/pkg/util/xorm/go.sum +++ b/pkg/util/xorm/go.sum @@ -937,8 +937,8 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -963,8 +963,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1079,8 +1079,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1110,8 +1110,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1128,8 +1128,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1207,8 +1207,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1233,16 +1233,16 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1566,8 +1566,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/public/app/api/clients/provisioning/endpoints.gen.ts b/public/app/api/clients/provisioning/endpoints.gen.ts index 58fa48fb9f1..c665973eb50 100644 --- a/public/app/api/clients/provisioning/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/endpoints.gen.ts @@ -1096,15 +1096,18 @@ export type ResourceList = { kind?: string; metadata?: ListMeta; }; +export type ErrorDetails = { + detail?: string; + field?: string; + type: string; +}; export type TestResults = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** HTTP status code */ code: number; - /** Optional details */ - details?: Unstructured; - /** Error descriptions */ - errors?: string[]; + /** Field related errors */ + errors?: ErrorDetails[]; /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; /** Is the connection healthy */ diff --git a/public/app/api/clients/provisioning/index.ts b/public/app/api/clients/provisioning/index.ts index 3aeb73d552f..5695306ad6e 100644 --- a/public/app/api/clients/provisioning/index.ts +++ b/public/app/api/clients/provisioning/index.ts @@ -13,6 +13,7 @@ import { JobList, Repository, RepositoryList, + ErrorDetails, } from './endpoints.gen'; import { createOnCacheEntryAdded } from './utils/createOnCacheEntryAdded'; @@ -100,9 +101,11 @@ export const provisioningAPI = generatedAPI.enhanceEndpoints({ dispatch(notifyApp(createErrorNotification('Error validating repository', e))); } else if (typeof e === 'object' && 'error' in e && isFetchError(e.error)) { if (Array.isArray(e.error.data.errors) && e.error.data.errors.length) { - dispatch( - notifyApp(createErrorNotification('Error validating repository', e.error.data.errors.join('\n'))) - ); + const nonFieldErrors = e.error.data.errors.filter((err: ErrorDetails) => !err.field); + // Only show notification if there are errors that don't have a field, field errors are handled by the form + if (nonFieldErrors.length > 0) { + dispatch(notifyApp(createErrorNotification('Error validating repository'))); + } } } } diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx index 89e95a4815d..9c1c08eb578 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx @@ -14,10 +14,16 @@ export const DEFAULT_EXTENSION_SIDEBAR_WIDTH = 300; export const MIN_EXTENSION_SIDEBAR_WIDTH = 100; export const MAX_EXTENSION_SIDEBAR_WIDTH = 700; +type ExtensionSidebarComponentProps = { + props?: Record; +}; + export function ExtensionSidebar() { const styles = getStyles(useTheme2()); - const { dockedComponentId, isEnabled } = useExtensionSidebarContext(); - const { components, isLoading } = usePluginComponents({ extensionPointId: EXTENSION_SIDEBAR_EXTENSION_POINT_ID }); + const { dockedComponentId, isEnabled, props = {} } = useExtensionSidebarContext(); + const { components, isLoading } = usePluginComponents({ + extensionPointId: EXTENSION_SIDEBAR_EXTENSION_POINT_ID, + }); if (isLoading || !dockedComponentId || !isEnabled) { return null; @@ -39,7 +45,7 @@ export function ExtensionSidebar() { return (
- +
); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx index 2055802bf22..660aa81e014 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.test.tsx @@ -1,9 +1,9 @@ import { render, screen, act } from '@testing-library/react'; -// import { render } from 'test/test-utils'; -import { store } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { store, EventBusSrv, EventBus } from '@grafana/data'; +import { config, getAppEvents, setAppEvents } from '@grafana/runtime'; import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; +import { OpenExtensionSidebarEvent } from 'app/types/events'; import { ExtensionSidebarContextProvider, @@ -52,10 +52,31 @@ const mockPluginMeta = { }; describe('ExtensionSidebarProvider', () => { + let subscribeSpy: jest.SpyInstance; + let originalAppEvents: EventBus; + let mockEventBus: EventBusSrv; + beforeEach(() => { jest.clearAllMocks(); + + originalAppEvents = getAppEvents(); + + mockEventBus = new EventBusSrv(); + subscribeSpy = jest.spyOn(mockEventBus, 'subscribe'); + + setAppEvents(mockEventBus); + (getExtensionPointPluginMeta as jest.Mock).mockReturnValue(new Map([[mockPluginMeta.pluginId, mockPluginMeta]])); + jest.replaceProperty(config.featureToggles, 'extensionSidebar', true); + + (store.get as jest.Mock).mockReturnValue(undefined); + (store.set as jest.Mock).mockImplementation(() => {}); + (store.delete as jest.Mock).mockImplementation(() => {}); + }); + + afterEach(() => { + setAppEvents(originalAppEvents); }); const TestComponent = () => { @@ -199,6 +220,114 @@ describe('ExtensionSidebarProvider', () => { expect(screen.getByTestId('available-components-size')).toHaveTextContent('1'); expect(screen.getByTestId('plugin-ids')).toHaveTextContent(permittedPluginMeta.pluginId); }); + + it('should subscribe to OpenExtensionSidebarEvent when feature is enabled', async () => { + render( + + + + ); + + expect(subscribeSpy).toHaveBeenCalledWith(OpenExtensionSidebarEvent, expect.any(Function)); + }); + + it('should not subscribe to OpenExtensionSidebarEvent when feature is disabled', () => { + jest.replaceProperty(config.featureToggles, 'extensionSidebar', false); + + render( + + + + ); + + expect(subscribeSpy).not.toHaveBeenCalled(); + }); + + it('should set dockedComponentId and props when receiving a valid OpenExtensionSidebarEvent', () => { + const TestComponentWithProps = () => { + const context = useExtensionSidebarContext(); + return ( +
+
{context.isOpen.toString()}
+
{context.dockedComponentId || 'undefined'}
+
{context.props ? JSON.stringify(context.props) : 'undefined'}
+
+ ); + }; + + render( + + + + ); + + expect(screen.getByTestId('is-open')).toHaveTextContent('false'); + expect(screen.getByTestId('props')).toHaveTextContent('undefined'); + + expect(subscribeSpy).toHaveBeenCalledWith(OpenExtensionSidebarEvent, expect.any(Function)); + act(() => { + // Get the event subscriber function + const [[, subscriberFn]] = subscribeSpy.mock.calls; + + // Call it directly with the test event + subscriberFn( + new OpenExtensionSidebarEvent({ + pluginId: 'grafana-investigations-app', + componentTitle: 'Test Component', + props: { testProp: 'test value' }, + }) + ); + }); + + expect(screen.getByTestId('is-open')).toHaveTextContent('true'); + expect(screen.getByTestId('props')).toHaveTextContent('{"testProp":"test value"}'); + const expectedComponentId = JSON.stringify({ + pluginId: 'grafana-investigations-app', + componentTitle: 'Test Component', + }); + expect(screen.getByTestId('docked-component-id')).toHaveTextContent(expectedComponentId); + }); + + it('should not open sidebar when receiving an OpenExtensionSidebarEvent with non-permitted plugin', () => { + render( + + + + ); + + expect(screen.getByTestId('is-open')).toHaveTextContent('false'); + + act(() => { + // Get the event subscriber function + const [[, subscriberFn]] = subscribeSpy.mock.calls; + + // Call it directly with the test event for a non-permitted plugin + subscriberFn( + new OpenExtensionSidebarEvent({ + pluginId: 'non-permitted-plugin', + componentTitle: 'Test Component', + }) + ); + }); + + expect(screen.getByTestId('is-open')).toHaveTextContent('false'); + }); + + it('should unsubscribe from OpenExtensionSidebarEvent on unmount', () => { + const unsubscribeMock = jest.fn(); + subscribeSpy.mockReturnValue({ + unsubscribe: unsubscribeMock, + }); + + const { unmount } = render( + + + + ); + + unmount(); + expect(unsubscribeMock).toHaveBeenCalled(); + }); }); describe('Utility Functions', () => { diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx index 6346a561555..8f64c6a6071 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx @@ -1,9 +1,10 @@ -import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; +import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react'; import { useLocalStorage } from 'react-use'; import { store, type ExtensionInfo } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, getAppEvents } from '@grafana/runtime'; import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; +import { OpenExtensionSidebarEvent } from 'app/types/events'; import { DEFAULT_EXTENSION_SIDEBAR_WIDTH } from './ExtensionSidebar'; @@ -45,6 +46,8 @@ type ExtensionSidebarContextType = { * Set the width of the extension sidebar. */ setExtensionSidebarWidth: (width: number) => void; + + props?: Record; }; export const ExtensionSidebarContext = createContext({ @@ -66,6 +69,7 @@ interface ExtensionSidebarContextProps { } export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarContextProps) => { + const [props, setProps] = useState | undefined>(undefined); const storedDockedPluginId = store.get(EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY); const [extensionSidebarWidth, setExtensionSidebarWidth] = useLocalStorage( EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY, @@ -104,6 +108,39 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo } const [dockedComponentId, setDockedComponentId] = useState(defaultDockedComponentId); + const setDockedComponentWithProps = useCallback( + (componentId: string | undefined, props?: Record) => { + setProps(props); + setDockedComponentId(componentId); + }, + [setDockedComponentId] + ); + + useEffect(() => { + if (!isEnabled) { + return; + } + + // handler to open the extension sidebar from plugins. this is done with the `helpers.openSidebar` function + const openSidebarHandler = (event: OpenExtensionSidebarEvent) => { + if ( + event.payload.pluginId && + event.payload.componentTitle && + PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(event.payload.pluginId) + ) { + setDockedComponentWithProps( + JSON.stringify({ pluginId: event.payload.pluginId, componentTitle: event.payload.componentTitle }), + event.payload.props + ); + } + }; + + const subscription = getAppEvents().subscribe(OpenExtensionSidebarEvent, openSidebarHandler); + return () => { + subscription.unsubscribe(); + }; + }, [isEnabled, setDockedComponentWithProps]); + // update the stored docked component id when it changes useEffect(() => { if (dockedComponentId) { @@ -119,10 +156,11 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo isEnabled, isOpen: isEnabled && dockedComponentId !== undefined, dockedComponentId, - setDockedComponentId, + setDockedComponentId: (componentId) => setDockedComponentWithProps(componentId, undefined), availableComponents, extensionSidebarWidth: extensionSidebarWidth ?? DEFAULT_EXTENSION_SIDEBAR_WIDTH, setExtensionSidebarWidth, + props, }} > {children} diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx index ccea31d0059..7b6823c56d3 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { store } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { EventBusSrv, store } from '@grafana/data'; +import { config, setAppEvents } from '@grafana/runtime'; import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; import { ExtensionSidebarContextProvider, useExtensionSidebarContext } from './ExtensionSidebarProvider'; @@ -73,6 +73,11 @@ describe('ExtensionToolbarItem', () => { (store.set as jest.Mock).mockClear(); (store.delete as jest.Mock).mockClear(); jest.replaceProperty(config.featureToggles, 'extensionSidebar', true); + setAppEvents(new EventBusSrv()); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('should not render when feature toggle is disabled', () => { diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx index be49778db62..4d8f7031db7 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx @@ -87,6 +87,9 @@ export function ExtensionToolbarItem() { function getStyles(theme: GrafanaTheme2) { return { button: css({ + // this is needed because with certain breakpoints the button will get `width: auto` + // and the icon will stretch + aspectRatio: '1 / 1 !important', width: '28px', height: '28px', padding: 0, diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 3dedb1ea0d5..88d61ad0665 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -13,6 +13,9 @@ import { setBookmark } from 'app/core/reducers/navBarTree'; import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index'; import { useDispatch, useSelector } from 'app/types'; +import { InviteUserButton } from '../../InviteUserButton/InviteUserButton'; +import { shouldRenderInviteUserButton } from '../../InviteUserButton/utils'; + import { MegaMenuHeader } from './MegaMenuHeader'; import { MegaMenuItem } from './MegaMenuItem'; import { usePinnedItems } from './hooks'; @@ -125,6 +128,11 @@ export const MegaMenu = memo( ))} + {shouldRenderInviteUserButton && ( +
+ +
+ )}
); @@ -162,6 +170,11 @@ const getStyles = (theme: GrafanaTheme2) => { width: MENU_WIDTH, }, }), + inviteNewMemberButton: css({ + display: 'flex', + padding: theme.spacing(1.5, 1, 1.5, 1), + borderTop: `1px solid ${theme.colors.border.weak}`, + }), dockMenuButton: css({ display: 'none', position: 'relative', diff --git a/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx b/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx deleted file mode 100644 index 452e899ca47..00000000000 --- a/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { reportInteraction } from '@grafana/runtime'; -import { Box, Button } from '@grafana/ui'; -import { config } from 'app/core/config'; -import { t } from 'app/core/internationalization'; -import { contextSrv } from 'app/core/services/context_srv'; -import { getExternalUserMngLinkUrl } from 'app/features/users/utils'; -import { AccessControlAction } from 'app/types'; - -import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; - -export function InviteUserButton() { - return config.externalUserMngLinkUrl && contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) ? ( - - - - - ) : null; -} diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 5db53ff730e..4ef7b20ce85 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -24,7 +24,6 @@ import { enrichHelpItem } from '../MegaMenu/utils'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; import { QuickAdd } from '../QuickAdd/QuickAdd'; -import { InviteUserButton } from './InviteUserButton'; import { ProfileButton } from './ProfileButton'; import { SignInLink } from './SignInLink'; import { SingleTopBarActions } from './SingleTopBarActions'; @@ -104,7 +103,6 @@ export const SingleTopBar = memo(function SingleTopBar({ )} - {config.featureToggles.inviteUserExperimental && !isSmallScreen && } {config.featureToggles.extensionSidebar && !isSmallScreen && } {!showToolbarLevel && actions} {!contextSrv.user.isSignedIn && } diff --git a/public/app/core/components/InviteUserButton/InviteUserButton.tsx b/public/app/core/components/InviteUserButton/InviteUserButton.tsx new file mode 100644 index 00000000000..6417bb2a066 --- /dev/null +++ b/public/app/core/components/InviteUserButton/InviteUserButton.tsx @@ -0,0 +1,21 @@ +import { Button } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; + +import { performInviteUserClick } from './utils'; + +export function InviteUserButton() { + return ( + + ); +} diff --git a/public/app/core/components/InviteUserButton/utils.ts b/public/app/core/components/InviteUserButton/utils.ts new file mode 100644 index 00000000000..708e5ff88d5 --- /dev/null +++ b/public/app/core/components/InviteUserButton/utils.ts @@ -0,0 +1,19 @@ +import { reportInteraction } from '@grafana/runtime'; +import { config } from 'app/core/config'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getExternalUserMngLinkUrl } from 'app/features/users/utils'; +import { AccessControlAction } from 'app/types'; + +export const shouldRenderInviteUserButton = + config.featureToggles.inviteUserExperimental && + config.externalUserMngLinkUrl && + contextSrv.hasPermission(AccessControlAction.OrgUsersAdd); + +export const performInviteUserClick = (placement: string, cnt: string) => { + reportInteraction('invite_user_button_clicked', { + placement, + }); + + const url = getExternalUserMngLinkUrl(cnt); + window.open(url.toString(), '_blank'); +}; diff --git a/public/app/features/alerting/unified/MuteTimings.test.tsx b/public/app/features/alerting/unified/MuteTimings.test.tsx index 3654f6106b3..e82b675176f 100644 --- a/public/app/features/alerting/unified/MuteTimings.test.tsx +++ b/public/app/features/alerting/unified/MuteTimings.test.tsx @@ -4,7 +4,6 @@ import { Route, Routes } from 'react-router-dom-v5-compat'; import { render, screen, userEvent, within } from 'test/test-utils'; import { byTestId } from 'testing-library-selector'; -import { config } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; @@ -19,6 +18,7 @@ import { AccessControlAction } from 'app/types'; import EditMuteTimingPage from './components/mute-timings/EditMuteTiming'; import NewMuteTimingPage from './components/mute-timings/NewMuteTiming'; +import { defaultConfig, muteTimeInterval } from './components/mute-timings/mocks'; import { grantUserPermissions, mockDataSource } from './mocks'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; @@ -59,21 +59,6 @@ const ui = { years: byTestId('mute-timing-years'), }; -const muteTimeInterval: MuteTimeInterval = { - name: 'default-mute', - time_intervals: [ - { - times: [ - { - start_time: '12:00', - end_time: '24:00', - }, - ], - days_of_month: ['15', '-1'], - months: ['august:december', 'march'], - }, - ], -}; const muteTimeInterval2: MuteTimeInterval = { name: 'default-mute2', time_intervals: [ @@ -90,26 +75,6 @@ const muteTimeInterval2: MuteTimeInterval = { ], }; -/** Alertmanager config where time intervals are stored in `mute_time_intervals` property */ -export const defaultConfig: AlertManagerCortexConfig = { - alertmanager_config: { - receivers: [{ name: 'default' }, { name: 'critical' }], - route: { - receiver: 'default', - group_by: ['alertname'], - routes: [ - { - matchers: ['env=prod', 'region!=EU'], - mute_time_intervals: [muteTimeInterval.name], - }, - ], - }, - templates: [], - mute_time_intervals: [muteTimeInterval], - }, - template_files: {}, -}; - /** Alertmanager config where time intervals are stored in `time_intervals` property */ const defaultConfigWithNewTimeIntervalsField: AlertManagerCortexConfig = { alertmanager_config: { @@ -200,6 +165,7 @@ describe('Mute timings', () => { grantUserPermissions(Object.values(AccessControlAction)); setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig); + setAlertmanagerConfig(dataSources.am.uid, defaultConfig); // TODO: Add this at a higher level to ensure that no tests depend on others running first // Without this, the selected alertmanager in a previous test can affect the next, meaning tests @@ -209,7 +175,7 @@ describe('Mute timings', () => { it('creates a new mute timing, with mute_time_intervals in config', async () => { const capture = captureRequests(); - renderMuteTimings('/alerting/routes/new'); + renderMuteTimings({ pathname: '/alerting/routes/new', search: `?alertmanager=${dataSources.am.name}` }); await screen.findByText(/add mute timing/i); @@ -276,10 +242,9 @@ describe('Mute timings', () => { it('prepopulates the form when editing a mute timing', async () => { const capture = captureRequests(); - renderMuteTimings({ pathname: '/alerting/routes/edit', - search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}`, + search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}&alertmanager=${dataSources.am.name}`, }); expect(await ui.nameField.find()).toBeInTheDocument(); @@ -318,7 +283,7 @@ describe('Mute timings', () => { }); it('form is invalid with duplicate mute timing name', async () => { - renderMuteTimings('/alerting/routes/new'); + renderMuteTimings({ pathname: '/alerting/routes/new', search: `?alertmanager=${dataSources.am.name}` }); await fillOutForm({ name: muteTimeInterval.name, days: '1' }); @@ -330,7 +295,7 @@ describe('Mute timings', () => { it('replaces mute timings in routes when the mute timing name is changed', async () => { renderMuteTimings({ pathname: '/alerting/routes/edit', - search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}`, + search: `?muteName=${encodeURIComponent(muteTimeInterval.name)}&alertmanager=${dataSources.am.name}`, }); expect(await ui.nameField.find()).toBeInTheDocument(); @@ -352,46 +317,40 @@ describe('Mute timings', () => { expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument(); }); - describe('alertingApiServer feature toggle', () => { - beforeEach(() => { - config.featureToggles.alertingApiServer = true; + it('allows creation of new mute timings', async () => { + renderMuteTimings('/alerting/routes/new'); + + await fillOutForm({ name: 'a new mute timing' }); + + await saveMuteTiming(); + await expectToHaveRedirectedToRoutesRoute(); + }); + + it('shows error when mute timing does not exist', async () => { + renderMuteTimings({ + pathname: '/alerting/routes/edit', + search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH + '_force_breakage'}`, }); - it('allows creation of new mute timings', async () => { - renderMuteTimings('/alerting/routes/new'); + expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument(); + }); - await fillOutForm({ name: 'a new mute timing' }); - - await saveMuteTiming(); - await expectToHaveRedirectedToRoutesRoute(); + it('loads edit form correctly and allows saving', async () => { + renderMuteTimings({ + pathname: '/alerting/routes/edit', + search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH}`, }); - it('shows error when mute timing does not exist', async () => { - renderMuteTimings({ - pathname: '/alerting/routes/edit', - search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH + '_force_breakage'}`, - }); + await saveMuteTiming(); + await expectToHaveRedirectedToRoutesRoute(); + }); - expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument(); + it('loads view form for provisioned interval', async () => { + renderMuteTimings({ + pathname: '/alerting/routes/edit', + search: `?muteName=${TIME_INTERVAL_NAME_FILE_PROVISIONED}`, }); - it('loads edit form correctly and allows saving', async () => { - renderMuteTimings({ - pathname: '/alerting/routes/edit', - search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH}`, - }); - - await saveMuteTiming(); - await expectToHaveRedirectedToRoutesRoute(); - }); - - it('loads view form for provisioned interval', async () => { - renderMuteTimings({ - pathname: '/alerting/routes/edit', - search: `?muteName=${TIME_INTERVAL_NAME_FILE_PROVISIONED}`, - }); - - expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument(); - }); + expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument(); }); }); diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx index dd124c0f9a6..c70d50a114e 100644 --- a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx +++ b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx @@ -21,7 +21,6 @@ import { TIME_INTERVAL_NAME_FILE_PROVISIONED, TIME_INTERVAL_NAME_HAPPY_PATH, } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s'; -import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { AlertManagerCortexConfig, @@ -140,13 +139,7 @@ const getRootRoute = async () => { return ui.rootRouteContainer.find(); }; -describe.each([ - // k8s API enabled - true, - // k8s API disabled - false, -])('NotificationPolicies with alertingApiServer=%p', (apiServerEnabled) => { - apiServerEnabled ? testWithFeatureToggles(['alertingApiServer']) : testWithFeatureToggles([]); +describe('NotificationPolicies', () => { beforeEach(() => { setupDataSources(...Object.values(dataSources)); grantUserPermissions([ @@ -369,22 +362,6 @@ describe.each([ }); }); -describe('Grafana alertmanager - config API', () => { - it('Converts matchers to object_matchers for grafana alertmanager', async () => { - const { user } = renderNotificationPolicies(); - - const policyIndex = 0; - await openEditModal(policyIndex); - - // Save policy to test that format is converted to object_matchers - await user.click(await ui.saveButton.find()); - - expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i); - - const updatedConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); - expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].object_matchers).toMatchSnapshot(); - }); -}); describe('Non-Grafana alertmanagers', () => { it.skip('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => { makeAllAlertmanagerConfigFetchFail(getErrorResponse('alertmanager storage object not found')); diff --git a/public/app/features/alerting/unified/Settings.test.tsx b/public/app/features/alerting/unified/Settings.test.tsx index 401b4b6fee7..d086ba7663d 100644 --- a/public/app/features/alerting/unified/Settings.test.tsx +++ b/public/app/features/alerting/unified/Settings.test.tsx @@ -1,5 +1,4 @@ import { screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { render } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; @@ -26,8 +25,9 @@ const ui = { statusReceiving: byText(/receiving grafana-managed alerts/i), statusNotReceiving: byText(/not receiving/i), - configurationDrawer: byRole('dialog', { name: 'Drawer title Internal Grafana Alertmanager' }), + configurationDrawer: byRole('dialog', { name: 'Drawer title Grafana built-in Alertmanager' }), editConfigurationButton: byRole('button', { name: /edit configuration/i }), + viewConfigurationButton: byRole('button', { name: /view configuration/i }), saveConfigurationButton: byRole('button', { name: /save/i }), enableButton: byRole('button', { name: 'Enable' }), @@ -54,7 +54,7 @@ describe('Alerting settings', () => { expect(ui.statusReceiving.get(ui.builtInAlertmanagerCard.get())).toBeInTheDocument(); - // check external altermanagers + // check external alertmanagers DataSourcesResponse.forEach((ds) => { // get the card for datasource const card = ui.alertmanagerCard(ds.name).get(); @@ -74,45 +74,36 @@ describe('Alerting settings', () => { }); it('should be able to view configuration', async () => { - render(); + const { user } = render(); // wait for loading to be done await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); // open configuration drawer const internalAMCard = ui.builtInAlertmanagerCard.get(); - const editInternal = ui.editConfigurationButton.get(internalAMCard); - await userEvent.click(editInternal); + await user.click(ui.viewConfigurationButton.get(internalAMCard)); + expect(await ui.configurationDrawer.find()).toBeInTheDocument(); - await waitFor(() => { - expect(ui.configurationDrawer.get()).toBeInTheDocument(); - }); - - await userEvent.click(ui.saveConfigurationButton.get()); - expect(ui.saveConfigurationButton.get()).toBeDisabled(); - - await waitFor(() => { - expect(ui.saveConfigurationButton.get()).toBeEnabled(); - }); + expect(ui.saveConfigurationButton.query()).not.toBeInTheDocument(); }); it('should be able to view versions', async () => { - render(); + const { user } = render(); // wait for loading to be done - await waitFor(() => expect(ui.builtInAlertmanagerSection.get()).toBeInTheDocument()); + expect(await ui.builtInAlertmanagerSection.find()).toBeInTheDocument(); // open configuration drawer const internalAMCard = ui.builtInAlertmanagerCard.get(); - const editInternal = ui.editConfigurationButton.get(internalAMCard); - await userEvent.click(editInternal); + await user.click(ui.viewConfigurationButton.get(internalAMCard)); + expect(await ui.configurationDrawer.find()).toBeInTheDocument(); await waitFor(() => { expect(ui.configurationDrawer.get()).toBeInTheDocument(); }); // click versions tab - await userEvent.click(ui.versionsTab.get()); + await user.click(ui.versionsTab.get()); await waitFor(() => { expect(screen.getByText(/last applied/i)).toBeInTheDocument(); diff --git a/public/app/features/alerting/unified/Templates.test.tsx b/public/app/features/alerting/unified/Templates.test.tsx index ac2ede75e76..b7ff2f7b3e4 100644 --- a/public/app/features/alerting/unified/Templates.test.tsx +++ b/public/app/features/alerting/unified/Templates.test.tsx @@ -8,7 +8,6 @@ import { byLabelText, byRole } from 'testing-library-selector'; import { CodeEditor } from '@grafana/ui'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; -import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; import { AccessControlAction } from 'app/types'; import Templates from './Templates'; @@ -76,15 +75,18 @@ const setup = (initialEntries: InitialEntry[]) => { ); }; +const slackTemplate = 'k8s-template%20with%20spaces-resource-name'; +const emailTemplate = 'k8s-custom-email-resource-name'; + describe('Templates routes', () => { it('allows duplication of template with spaces in name', async () => { - setup([navUrl.duplicate('template%20with%20spaces')]); + setup([navUrl.duplicate(slackTemplate)]); expect(await screen.findByText('Edit payload')).toBeInTheDocument(); }); it('allows editing of template with spaces in name', async () => { - setup([navUrl.edit('template%20with%20spaces')]); + setup([navUrl.edit(slackTemplate)]); expect(await screen.findByText('Edit payload')).toBeInTheDocument(); }); @@ -99,7 +101,7 @@ describe('Templates routes', () => { }); it('should pass name validation when editing existing template', async () => { - const { user } = setup([navUrl.edit('custom-email')]); + const { user } = setup([navUrl.edit(emailTemplate)]); const titleElement = await ui.form.title.find(); await waitFor(() => { @@ -115,21 +117,6 @@ describe('Templates routes', () => { }); }); - it('should display error message when creating new template with duplicate name', async () => { - const { user } = setup([navUrl.new]); - - const titleElement = await ui.form.title.find(); - await user.type(titleElement, 'custom-email'); - - await user.click(ui.form.saveButton.get()); - - expect(screen.getByText('Another template with this name already exists')).toBeInTheDocument(); - }); -}); - -describe('Templates K8s API', () => { - testWithFeatureToggles(['alertingApiServer']); - it('form edit renders with correct form values', async () => { setup([navUrl.edit('k8s-custom-email-resource-name')]); diff --git a/public/app/features/alerting/unified/__snapshots__/NotificationPoliciesPage.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/NotificationPoliciesPage.test.tsx.snap index e2d5712c238..b6c6a2c31fb 100644 --- a/public/app/features/alerting/unified/__snapshots__/NotificationPoliciesPage.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/NotificationPoliciesPage.test.tsx.snap @@ -1,30 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Grafana alertmanager - config API Converts matchers to object_matchers for grafana alertmanager 1`] = ` -[ - [ - "sub1matcher1", - "=", - "sub1value1", - ], - [ - "sub1matcher2", - "=", - "sub1value2", - ], - [ - "sub1matcher3", - "=~", - "sub1value3", - ], - [ - "sub1matcher4", - "=~", - "sub1value4", - ], -] -`; - exports[`Non-Grafana alertmanagers Keeps matchers for non-grafana alertmanager sources 1`] = ` [ "hello="world"", diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx index efe299c00cf..795f984e99c 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPointHeader.tsx @@ -67,7 +67,6 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade * Used to determine whether to show the "Unused" badge */ const isReferencedByAnything = usingK8sApi ? Boolean(numberOfPolicies || numberOfRules) : policies.length > 0; - /** Does the current user have permissions to edit the contact point? */ const hasAbilityToEdit = canEditEntity(contactPoint) || editAllowed; /** Can the contact point actually be edited via the UI? */ diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx index 633e3d30248..33e94e32f9d 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.test.tsx @@ -4,7 +4,7 @@ import { render, screen, userEvent, waitFor, waitForElementToBeRemoved, within } import { selectors } from '@grafana/e2e-selectors'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; -import { flushMicrotasks, testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; +import { flushMicrotasks } from 'app/features/alerting/unified/test/test-utils'; import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; @@ -60,6 +60,16 @@ const basicContactPoint: ContactPointWithMetadata = { grafana_managed_receiver_configs: [], }; +const basicContactPointInUse: ContactPointWithMetadata = { + ...basicContactPoint, + metadata: { + annotations: { + [K8sAnnotations.InUseRules]: '1', + [K8sAnnotations.InUseRoutes]: '1', + }, + }, +}; + const contactPointWithEverything: ContactPointWithMetadata = { ...basicContactPoint, metadata: { @@ -171,9 +181,8 @@ describe('contact points', () => { expect(screen.getByRole('link', { name: 'add contact point' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'export all' })).toBeInTheDocument(); - // 2 of them are unused by routes in the mock response const unusedBadge = screen.getAllByLabelText('unused'); - expect(unusedBadge).toHaveLength(3); + expect(unusedBadge).toHaveLength(4); const viewProvisioned = screen.getByRole('link', { name: 'view-action' }); expect(viewProvisioned).toBeInTheDocument(); @@ -203,25 +212,13 @@ describe('contact points', () => { // should disable create contact point expect(screen.getByRole('link', { name: 'add contact point' })).toHaveAttribute('aria-disabled', 'true'); - // there should be no edit buttons - expect(screen.queryAllByRole('link', { name: 'edit-action' })).toHaveLength(0); + // edit permission is based on API response - we should have 3 buttons + const editButtons = await screen.findAllByRole('link', { name: 'edit-action' }); + expect(editButtons).toHaveLength(3); - // there should be view buttons though + // there should be view buttons though - one for provisioned, and one for the un-editable contact point const viewButtons = screen.getAllByRole('link', { name: 'view-action' }); - expect(viewButtons).toHaveLength(5); - - // delete should be disabled in the "more" actions - const moreButtons = screen.queryAllByRole('button', { name: /More/ }); - expect(moreButtons).toHaveLength(5); - - // check if all of the delete buttons are disabled - for await (const button of moreButtons) { - await user.click(button); - const deleteButton = screen.queryByRole('menuitem', { name: 'delete' }); - expect(deleteButton).toBeDisabled(); - // click outside the menu to close it otherwise we can't interact with the rest of the page - await user.click(document.body); - } + expect(viewButtons).toHaveLength(2); // check buttons in Notification Templates const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' }); @@ -297,7 +294,7 @@ describe('contact points', () => { }, ]; - const { user } = renderWithProvider(); + const { user } = renderWithProvider(); expect(screen.getByRole('link', { name: /1 notification policy/ })).toBeInTheDocument(); @@ -431,9 +428,7 @@ describe('contact points', () => { }); }); - describe('alertingApiServer enabled', () => { - testWithFeatureToggles(['alertingApiServer']); - + describe('Grafana alertmanager', () => { beforeEach(() => { grantUserPermissions([ AccessControlAction.AlertingNotificationsRead, diff --git a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx index 9ee0fae551b..3a7895c1a29 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditContactPoint.test.tsx @@ -1,7 +1,7 @@ import 'core-js/stable/structured-clone'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { clickSelectOption } from 'test/helpers/selectOptionInTest'; -import { render, screen } from 'test/test-utils'; +import { render, screen, within } from 'test/test-utils'; import EditContactPoint from 'app/features/alerting/unified/components/contact-points/EditContactPoint'; import { AccessControlAction } from 'app/types'; @@ -15,6 +15,20 @@ const Index = () => { return
redirected
; }; +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: function CodeEditor({ value, onBlur }: { value: string; onBlur: (newValue: string) => void }) { + return onBlur(e.currentTarget.value)} />; + }, +})); + +jest.mock( + 'react-virtualized-auto-sizer', + () => + ({ children }: { children: ({ height, width }: { height: number; width: number }) => JSX.Element }) => + children({ height: 500, width: 400 }) +); + const renderEditContactPoint = (contactPointUid: string) => render( @@ -30,8 +44,7 @@ beforeEach(() => { grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]); }); -const getTemplatePreviewContent = async () => - await screen.findByRole('presentation', { description: /Preview with the default payload/i }); +const getTemplatePreviewContent = async () => within(screen.getByTestId('template-preview')).getByTestId('mockeditor'); const templatesSelectorTestId = 'existing-templates-selector'; @@ -44,11 +57,11 @@ describe('Edit contact point', () => { await user.click(await screen.findByText(/optional email settings/i)); await user.click(await screen.findByRole('button', { name: /edit message/i })); expect(await screen.findByRole('dialog', { name: /edit message/i })).toBeInTheDocument(); - expect(await getTemplatePreviewContent()).toHaveTextContent(/some example preview for slack-template/i); + expect(await getTemplatePreviewContent()).toHaveValue(`some example preview for {{ template "slack-template" . }}`); // Change the preset template and check that the preview updates correctly await clickSelectOption(screen.getByTestId(templatesSelectorTestId), 'custom-email'); - expect(await getTemplatePreviewContent()).toHaveTextContent(/some example preview for custom-email/i); + expect(await getTemplatePreviewContent()).toHaveValue(`some example preview for {{ template "custom-email" . }}`); // Close the drawer await user.click(screen.getByRole('button', { name: /^save$/i })); @@ -62,7 +75,7 @@ describe('Edit contact point', () => { await user.click(screen.getByRole('radio', { name: /select notification template/i })); await clickSelectOption(screen.getByTestId(templatesSelectorTestId), 'slack-template'); - expect(await getTemplatePreviewContent()).toHaveTextContent(/some example preview for slack-template/i); + expect(await getTemplatePreviewContent()).toHaveValue(`some example preview for {{ template "slack-template" . }}`); // Close the drawer await user.click(screen.getByRole('button', { name: /^save$/i })); diff --git a/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx index a9ec6208599..ee5cd458b8d 100644 --- a/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/NotificationTemplates.test.tsx @@ -1,7 +1,6 @@ import { render, screen, within } from 'test/test-utils'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; -import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; import { AccessControlAction } from 'app/types'; import { setupMswServer } from '../../mockApi'; @@ -74,44 +73,40 @@ describe('NotificationTemplates', () => { expect(within(provisionedRow).getByText('Provisioned')).toBeInTheDocument(); }); - describe('k8s API', () => { - testWithFeatureToggles(['alertingApiServer']); + it('Should render templates table with the correct rows', async () => { + renderWithProvider(); - it('Should render templates table with the correct rows', async () => { - renderWithProvider(); + const slackRow = await screen.findByRole('row', { name: /slack-template/i }); + expect(within(slackRow).getByRole('cell', { name: /slack-template/i })).toBeInTheDocument(); - const slackRow = await screen.findByRole('row', { name: /slack-template/i }); - expect(within(slackRow).getByRole('cell', { name: /slack-template/i })).toBeInTheDocument(); + const emailRow = await screen.findByRole('row', { name: /custom-email/i }); + expect(within(emailRow).getByRole('cell', { name: /custom-email/i })).toBeInTheDocument(); - const emailRow = await screen.findByRole('row', { name: /custom-email/i }); - expect(within(emailRow).getByRole('cell', { name: /custom-email/i })).toBeInTheDocument(); + const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i }); + expect(within(provisionedRow).getByRole('cell', { name: /provisioned-template/i })).toBeInTheDocument(); + }); - const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i }); - expect(within(provisionedRow).getByRole('cell', { name: /provisioned-template/i })).toBeInTheDocument(); - }); + it('Should provisioned badge for provisioned template', async () => { + renderWithProvider(); - it('Should provisioned badge for provisioned template', async () => { - renderWithProvider(); + const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i }); + expect(within(provisionedRow).getByText('Provisioned')).toBeInTheDocument(); + }); - const provisionedRow = await screen.findByRole('row', { name: /provisioned-template/i }); - expect(within(provisionedRow).getByText('Provisioned')).toBeInTheDocument(); - }); + it('Should delete template', async () => { + const { user } = renderWithProvider(); - it('Should delete template', async () => { - const { user } = renderWithProvider(); + const emailRow = await screen.findByRole('row', { name: /custom-email/i }); + const deleteEmailButton = within(emailRow).getByRole('button', { name: /delete template/i }); - const emailRow = await screen.findByRole('row', { name: /custom-email/i }); - const deleteEmailButton = within(emailRow).getByRole('button', { name: /delete template/i }); + await user.click(deleteEmailButton); - await user.click(deleteEmailButton); + const confirmDeleteButton = await screen.findByRole('button', { name: /Yes, delete/i }); + await user.click(confirmDeleteButton); - const confirmDeleteButton = await screen.findByRole('button', { name: /Yes, delete/i }); - await user.click(confirmDeleteButton); - - expect(screen.queryByRole('row', { name: /custom-email/i })).not.toBeInTheDocument(); - expect(await screen.findByRole('status', { name: 'Template deleted' })).toHaveTextContent( - 'Template custom-email has been deleted' - ); - }); + expect(screen.queryByRole('row', { name: /custom-email/i })).not.toBeInTheDocument(); + expect(await screen.findByRole('status', { name: 'Template deleted' })).toHaveTextContent( + 'Template custom-email has been deleted' + ); }); }); diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap index 00d2cbdd9e3..ddb0a25c9e2 100644 --- a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap @@ -30,6 +30,17 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, ], "id": "grafana-default-email", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "false", + "grafana.com/access/canWrite": "false", + "grafana.com/inUse/routes": "1", + "grafana.com/inUse/rules": "1", + "grafana.com/provenance": "none", + }, + "uid": "grafana-default-email", + }, "name": "grafana-default-email", "policies": [ { @@ -69,6 +80,17 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, ], "id": "lotsa-emails", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "none", + }, + "uid": "lotsa-emails", + }, "name": "lotsa-emails", "policies": [], "provisioned": false, @@ -94,6 +116,17 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, ], "id": "OnCall Conctact point", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "none", + }, + "uid": "OnCall Conctact point", + }, "name": "OnCall Conctact point", "policies": [], "provisioned": false, @@ -125,6 +158,17 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, ], "id": "provisioned-contact-point", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "api", + }, + "uid": "provisioned-contact-point", + }, "name": "provisioned-contact-point", "policies": [ { @@ -186,6 +230,17 @@ exports[`useContactPoints should return contact points with status 1`] = ` }, ], "id": "Slack with multiple channels", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "none", + }, + "uid": "Slack with multiple channels", + }, "name": "Slack with multiple channels", "policies": [], "provisioned": false, @@ -226,6 +281,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, ], "id": "grafana-default-email", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "false", + "grafana.com/access/canWrite": "false", + "grafana.com/inUse/routes": "1", + "grafana.com/inUse/rules": "1", + "grafana.com/provenance": "none", + }, + "uid": "grafana-default-email", + }, "name": "grafana-default-email", "policies": [ { @@ -265,6 +331,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, ], "id": "lotsa-emails", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "none", + }, + "uid": "lotsa-emails", + }, "name": "lotsa-emails", "policies": [], "provisioned": false, @@ -293,6 +370,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, ], "id": "OnCall Conctact point", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "none", + }, + "uid": "OnCall Conctact point", + }, "name": "OnCall Conctact point", "policies": [], "provisioned": false, @@ -324,6 +412,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, ], "id": "provisioned-contact-point", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "api", + }, + "uid": "provisioned-contact-point", + }, "name": "provisioned-contact-point", "policies": [ { @@ -385,6 +484,17 @@ exports[`useContactPoints when having oncall plugin installed and no alert manag }, ], "id": "Slack with multiple channels", + "metadata": { + "annotations": { + "grafana.com/access/canAdmin": "true", + "grafana.com/access/canDelete": "true", + "grafana.com/access/canWrite": "true", + "grafana.com/inUse/routes": "0", + "grafana.com/inUse/rules": "0", + "grafana.com/provenance": "none", + }, + "uid": "Slack with multiple channels", + }, "name": "Slack with multiple channels", "policies": [], "provisioned": false, diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx index c7dab986d29..59f38e5a4fe 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx @@ -2,7 +2,6 @@ import { renderHook, waitFor } from '@testing-library/react'; import { ReactNode } from 'react'; import { getWrapper } from 'test/test-utils'; -import { config } from '@grafana/runtime'; import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure'; import { setOnCallIntegrations } from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins'; import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; @@ -21,8 +20,7 @@ const wrapper = ({ children }: { children: ReactNode }) => { setupMswServer(); -const getHookResponse = async (featureToggleEnabled: boolean) => { - config.featureToggles.alertingApiServer = featureToggleEnabled; +const getHookResponse = async () => { const { result } = renderHook( () => useContactPointsWithStatus({ @@ -61,41 +59,13 @@ describe('useContactPoints', () => { it('should return contact points with status', async () => { disablePlugin(SupportedPlugin.OnCall); - const snapshot = await getHookResponse(false); + const snapshot = await getHookResponse(); expect(snapshot).toMatchSnapshot(); }); - it('returns ~matching responses with and without alertingApiServer', async () => { - // Compare the responses between the two implementations, but do not consider: - // - ID: k8s API will return id properties, but the AM config will fall back to the name of the contact point. - // These will be different, so we don't want to compare them - // - Metadata: k8s API includes metadata, AM config does not - - const snapshotAmConfig = await getHookResponse(false); - const snapshotAlertingApiServer = await getHookResponse(true); - - const amContactPoints = snapshotAmConfig.contactPoints.map((receiver) => { - const { id, ...rest } = receiver; - return rest; - }); - - const k8sContactPoints = snapshotAlertingApiServer.contactPoints.map((receiver) => { - const { id, metadata, ...rest } = receiver; - return rest; - }); - - expect({ - ...snapshotAmConfig, - contactPoints: amContactPoints, - }).toEqual({ - ...snapshotAlertingApiServer, - contactPoints: k8sContactPoints, - }); - }); - describe('when having oncall plugin installed and no alert manager config data', () => { it('should return contact points with oncall metadata', async () => { - const snapshot = await getHookResponse(false); + const snapshot = await getHookResponse(); expect(snapshot).toMatchSnapshot(); }); }); diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx index 8be77b512d8..ff57ca0527a 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -1,12 +1,9 @@ import { render, screen, userEvent, within } from 'test/test-utils'; -import { config } from '@grafana/runtime'; -import { defaultConfig } from 'app/features/alerting/unified/MuteTimings.test'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setMuteTimingsListError } from 'app/features/alerting/unified/mocks/server/configure'; import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; -import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { grantUserPermissions } from '../../mocks'; @@ -15,8 +12,9 @@ import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { MuteTimingsTable } from './MuteTimingsTable'; +import { defaultConfig } from './mocks'; -const renderWithProvider = (alertManagerSource?: string) => { +const renderWithProvider = (alertManagerSource = GRAFANA_RULES_SOURCE_NAME) => { return render( @@ -27,10 +25,15 @@ const renderWithProvider = (alertManagerSource?: string) => { setupMswServer(); describe('MuteTimingsTable', () => { + beforeEach(() => { + window.localStorage.clear(); + // setupDataSources(); + }); + describe('with necessary permissions', () => { beforeEach(() => { setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig); - config.featureToggles.alertingApiServer = false; + grantUserPermissions([ AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite, @@ -46,11 +49,10 @@ describe('MuteTimingsTable', () => { }); it("shows individual 'export' drawer when allowed and supported, and can close", async () => { - const user = userEvent.setup(); - renderWithProvider(); + const { user } = renderWithProvider(); const table = await screen.findByTestId('dynamic-table'); - const exportMuteTiming = await within(table).findByText(/export/i); - await user.click(exportMuteTiming); + const exportMuteTiming = await within(table).findAllByText(/export/i); + await user.click(exportMuteTiming[0]); expect(await screen.findByRole('dialog', { name: /drawer title export/i })).toBeInTheDocument(); @@ -64,25 +66,6 @@ describe('MuteTimingsTable', () => { expect(screen.queryByRole('button', { name: /export all/i })).not.toBeInTheDocument(); }); - it('deletes interval', async () => { - // TODO: Don't use captureRequests for this, move to stateful mock server instead - // and check that the interval is no longer in the list - const capture = captureRequests(); - const user = userEvent.setup(); - renderWithProvider(); - - await user.click((await screen.findAllByText(/delete/i))[0]); - await user.click(await screen.findByRole('button', { name: /delete/i })); - - const requests = await capture; - const amConfigUpdateRequest = requests.find( - (r) => r.url.includes('/alertmanager/grafana/config/api/v1/alerts') && r.method === 'POST' - ); - - const body: AlertManagerCortexConfig = await amConfigUpdateRequest?.clone().json(); - expect(body.alertmanager_config.mute_time_intervals).toHaveLength(0); - }); - it('allow cancelling deletion', async () => { // TODO: Don't use captureRequests for this, move to stateful mock server instead // and check that the interval is still in the list @@ -100,34 +83,9 @@ describe('MuteTimingsTable', () => { expect(amConfigUpdateRequest).toBeUndefined(); }); - }); - describe('without necessary permissions', () => { - beforeEach(() => { - grantUserPermissions([]); - }); - - it('does not show export button when not allowed ', async () => { - renderWithProvider(); - expect(screen.queryByRole('button', { name: /export all/i })).not.toBeInTheDocument(); - }); - }); - - describe('using alertingApiServer feature toggle', () => { - beforeEach(() => { - config.featureToggles.alertingApiServer = true; - grantUserPermissions([ - AccessControlAction.AlertingNotificationsRead, - AccessControlAction.AlertingNotificationsWrite, - ]); - }); - - afterEach(() => { - config.featureToggles.alertingApiServer = false; - }); - - it('shows list of intervals from k8s API', async () => { - renderWithProvider(); + it('shows list of intervals from API', async () => { + renderWithProvider(GRAFANA_RULES_SOURCE_NAME); expect(await screen.findByTestId('dynamic-table')).toBeInTheDocument(); expect(await screen.findByText('Provisioned')).toBeInTheDocument(); @@ -157,4 +115,15 @@ describe('MuteTimingsTable', () => { expect(deleteRequest).toBeDefined(); }); }); + + describe('without necessary permissions', () => { + beforeEach(() => { + grantUserPermissions([]); + }); + + it('does not show export button when not allowed ', async () => { + renderWithProvider(); + expect(screen.queryByRole('button', { name: /export all/i })).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/alerting/unified/components/mute-timings/mocks.ts b/public/app/features/alerting/unified/components/mute-timings/mocks.ts new file mode 100644 index 00000000000..ab46dd335ea --- /dev/null +++ b/public/app/features/alerting/unified/components/mute-timings/mocks.ts @@ -0,0 +1,36 @@ +import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; + +export const muteTimeInterval: MuteTimeInterval = { + name: 'default-mute', + time_intervals: [ + { + times: [ + { + start_time: '12:00', + end_time: '24:00', + }, + ], + days_of_month: ['15', '-1'], + months: ['august:december', 'march'], + }, + ], +}; +/** Alertmanager config where time intervals are stored in `mute_time_intervals` property */ +export const defaultConfig: AlertManagerCortexConfig = { + alertmanager_config: { + receivers: [{ name: 'default' }, { name: 'critical' }], + route: { + receiver: 'default', + group_by: ['alertname'], + routes: [ + { + matchers: ['env=prod', 'region!=EU'], + mute_time_intervals: [muteTimeInterval.name], + }, + ], + }, + templates: [], + mute_time_intervals: [muteTimeInterval], + }, + template_files: {}, +}; diff --git a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx index 939e69b099e..3929c478053 100644 --- a/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/NewReceiverView.test.tsx @@ -2,13 +2,12 @@ import { Route, Routes } from 'react-router-dom-v5-compat'; import { render, screen } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector'; -import { makeAlertmanagerConfigUpdateFail } from 'app/features/alerting/unified/mocks/server/configure'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { AccessControlAction } from 'app/types'; import { setupMswServer } from '../../mockApi'; import { grantUserPermissions } from '../../mocks'; -import { testWithFeatureToggles } from '../../test/test-utils'; +import { makeAllK8sEndpointsFail } from '../../mocks/server/configure'; import NewReceiverView from './NewReceiverView'; @@ -33,9 +32,7 @@ beforeEach(() => { grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]); }); -describe('alerting API server enabled', () => { - testWithFeatureToggles(['alertingApiServer']); - +describe('new receiver', () => { it('can create a receiver', async () => { const { user } = renderForm(); @@ -52,9 +49,7 @@ describe('alerting API server enabled', () => { expect(await screen.findByText(/redirected/i)).toBeInTheDocument(); }); -}); -describe('alerting API server disabled', () => { it('should be able to test and save a receiver', async () => { const capture = captureRequests(); @@ -90,23 +85,16 @@ describe('alerting API server disabled', () => { const requests = await capture; const testRequest = requests.find((r) => r.url.endsWith('/config/api/v1/receivers/test')); - const saveRequest = requests.find( - (r) => r.url.endsWith('/api/alertmanager/grafana/config/api/v1/alerts') && r.method === 'POST' - ); + const saveRequest = requests.find((r) => r.url.endsWith('/receivers') && r.method === 'POST'); const testBody = await testRequest?.json(); - const fullSaveBody = await saveRequest?.json(); - - // Only snapshot and check the receivers, as we don't want other tests to break this - // just because we added something new to the mock config - const saveBody = fullSaveBody.alertmanager_config.receivers; + const saveBody = await saveRequest?.json(); expect([testBody]).toMatchSnapshot(); expect([saveBody]).toMatchSnapshot(); }); it('does not redirect when creating contact point and API errors', async () => { - makeAlertmanagerConfigUpdateFail(); const { user } = renderForm(); await user.type(await ui.inputs.name.find(), 'receiver that should fail'); @@ -114,6 +102,8 @@ describe('alerting API server disabled', () => { await user.clear(email); await user.type(email, 'tester@grafana.com'); + makeAllK8sEndpointsFail('someerror'); + await user.click(ui.saveContactButton.get()); expect(screen.queryByText(/redirected/i)).not.toBeInTheDocument(); diff --git a/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts b/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts index a53bb621b15..c82ea2f74aa 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts +++ b/public/app/features/alerting/unified/components/receivers/TemplateDataExamples.ts @@ -145,4 +145,43 @@ Alert annotations: {{ len .Annotations.SortedPairs }} - RunbookURL: {{ .Annotations.runbook_url}} {{ end -}}`, }, + { + description: 'Create JSON payload for webhook contact point', + example: `{{- /* Example displaying a custom JSON payload for a webhook contact point.*/ -}} +{{- /* Edit the template name and template content as needed. */ -}} +{{- /* Variables defined in the webhook contact point can be accessed in .Vars but will not be previewable. */ -}} +{{ define "webhook.custom.payload" -}} + {{ coll.Dict + "receiver" .Receiver + "status" .Status + "alerts" (tmpl.Exec "webhook.custom.simple_alerts" .Alerts | data.JSON) + "groupLabels" .GroupLabels + "commonLabels" .CommonLabels + "commonAnnotations" .CommonAnnotations + "externalURL" .ExternalURL + "version" "1" + "orgId" (index .Alerts 0).OrgID + "truncatedAlerts" .TruncatedAlerts + "groupKey" .GroupKey + "state" (tmpl.Inline "{{ if eq .Status \\"resolved\\" }}ok{{ else }}alerting{{ end }}" . ) + "allVariables" .Vars + "title" (tmpl.Exec "default.title" . ) + "message" (tmpl.Exec "default.message" . ) + | data.ToJSONPretty " "}} +{{- end }} + +{{- /* Example showcasing embedding json templates in other json templates. */ -}} +{{ define "webhook.custom.simple_alerts" -}} + {{- $alerts := coll.Slice -}} + {{- range . -}} + {{ $alerts = coll.Append (coll.Dict + "status" .Status + "labels" .Labels + "startsAt" .StartsAt + "endsAt" .EndsAt + ) $alerts}} + {{- end -}} + {{- $alerts | data.ToJSON -}} +{{- end }}`, + }, ]; diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 4728dbdb312..bf2f7167bc6 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -336,6 +336,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: function CodeEditor({ value, onBlur }: { value: string; onBlur: (newValue: string) => void }) { + return onBlur(e.currentTarget.value)} />; + }, +})); + jest.mock( 'react-virtualized-auto-sizer', () => @@ -50,6 +57,7 @@ describe('TemplatePreview component', () => { , @@ -66,6 +74,7 @@ describe('TemplatePreview component', () => { , @@ -81,6 +90,7 @@ describe('TemplatePreview component', () => { , @@ -100,6 +110,7 @@ describe('TemplatePreview component', () => { , @@ -123,6 +134,7 @@ describe('TemplatePreview component', () => { , @@ -133,8 +145,11 @@ describe('TemplatePreview component', () => { await waitFor(() => { expect(previews()).toHaveLength(2); }); - expect(previews()[0]).toHaveTextContent('This is the template result bla bla bla'); - expect(previews()[1]).toHaveTextContent('This is the template2 result bla bla bla'); + const previewItems = previews(); + expect(within(previewItems[0]).getByRole('banner')).toHaveTextContent('template1'); + expect(within(previewItems[0]).getByTestId('mockeditor')).toHaveValue('This is the template result bla bla bla'); + expect(within(previewItems[1]).getByRole('banner')).toHaveTextContent('template2'); + expect(within(previewItems[1]).getByTestId('mockeditor')).toHaveValue('This is the template2 result bla bla bla'); }); it('Should render preview response with some errors, if payload has correct format ', async () => { @@ -151,6 +166,7 @@ describe('TemplatePreview component', () => { , @@ -165,6 +181,39 @@ describe('TemplatePreview component', () => { expect(alerts()[1]).toHaveTextContent(/Unexpected "{" in operand/i); const previewContent = screen.getByRole('listitem'); - expect(previewContent).toHaveTextContent('This is the template result bla bla bla'); + expect(within(previewContent).getByTestId('mockeditor')).toHaveValue('This is the template result bla bla bla'); }); }); + +it('Should render preview type , if response contains valid json ', async () => { + const response: TemplatePreviewResponse = { + results: [ + { name: 'template_text', text: 'This is the template result bla bla bla' }, + { name: 'template_valid', text: '{"test":"value","test2":"value2"}' }, + { name: 'template_invalid', text: '{"test":"value","test2":"value2",}' }, + ], + }; + mockPreviewTemplateResponse(server, response); + render( + , + { wrapper: getProviderWraper() } + ); + + const previews = ui.resultItems.getAll; + await waitFor(() => { + expect(previews()).toHaveLength(3); + }); + const previewItems = previews(); + expect(within(previewItems[0]).getByRole('banner')).toHaveTextContent('template_text'); + expect(within(previewItems[0]).getByRole('banner')).toHaveTextContent('plaintext'); + expect(within(previewItems[1]).getByRole('banner')).toHaveTextContent('template_valid'); + expect(within(previewItems[1]).getByRole('banner')).toHaveTextContent('json'); + expect(within(previewItems[2]).getByRole('banner')).toHaveTextContent('template_invalid'); + expect(within(previewItems[2]).getByRole('banner')).toHaveTextContent('plaintext'); +}); diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx index 179d01c3588..d9e86c083e7 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx @@ -1,39 +1,35 @@ import { css, cx } from '@emotion/css'; import { compact, uniqueId } from 'lodash'; import * as React from 'react'; -import { useFormContext } from 'react-hook-form'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, Box, Button, useStyles2 } from '@grafana/ui'; +import { Alert, Box, Button, CodeEditor, useStyles2 } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi'; import { stringifyErrorLike } from '../../utils/misc'; import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader'; -import type { TemplateFormValues } from './TemplateForm'; import { usePreviewTemplate } from './usePreviewTemplate'; export function TemplatePreview({ payload, templateName, + templateContent, payloadFormatError, setPayloadFormatError, className, }: { payload: string; templateName: string; + templateContent: string; payloadFormatError: string | null; setPayloadFormatError: (value: React.SetStateAction) => void; className?: string; }) { const styles = useStyles2(getStyles); - const { watch } = useFormContext(); - - const templateContent = watch('content'); - const { data, isLoading, @@ -73,14 +69,41 @@ function PreviewResultViewer({ previews }: { previews: TemplatePreviewResult[] } // If there is only one template, we don't need to show the name const singleTemplate = previews.length === 1; + const isValidJson = (text: string) => { + try { + JSON.parse(text); + return true; + } catch { + return false; + } + }; + return ( -
    - {previews.map((preview) => ( -
  • - {singleTemplate ? null :
    {preview.name}
    } -
    {preview.text ?? ''}
    -
  • - ))} +
      + {previews.map((preview) => { + const language = isValidJson(preview.text) ? 'json' : 'plaintext'; + return ( +
    • + {singleTemplate ? null : ( +
      + {preview.name} +
      {language}
      +
      + )} + +
    • + ); + })}
    ); } @@ -101,6 +124,11 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderRadius: theme.shape.radius.default, border: `1px solid ${theme.colors.border.medium}`, }), + editorContainer: css({ + width: '100%', + height: '100%', + border: 'none', + }), viewerContainer: ({ height }: { height: number }) => css({ height, @@ -120,20 +148,20 @@ const getStyles = (theme: GrafanaTheme2) => ({ height: 'inherit', }), header: css({ + display: 'flex', + justifyContent: 'space-between', fontSize: theme.typography.bodySmall.fontSize, padding: theme.spacing(1, 2), borderBottom: `1px solid ${theme.colors.border.medium}`, backgroundColor: theme.colors.background.secondary, }), + language: css({ + marginLeft: 'auto', + fontStyle: 'italic', + }), errorText: css({ color: theme.colors.error.text, }), - pre: css({ - backgroundColor: 'transparent', - margin: 0, - border: 'none', - padding: theme.spacing(2), - }), }, }); diff --git a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap index 9141fd5246f..f4c781df037 100644 --- a/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap +++ b/public/app/features/alerting/unified/components/receivers/__snapshots__/NewReceiverView.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`alerting API server disabled should be able to test and save a receiver 1`] = ` +exports[`new receiver should be able to test and save a receiver 1`] = ` [ { "alert": { @@ -32,108 +32,16 @@ exports[`alerting API server disabled should be able to test and save a receiver ] `; -exports[`alerting API server disabled should be able to test and save a receiver 2`] = ` +exports[`new receiver should be able to test and save a receiver 2`] = ` [ - [ - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "grafana-default-email", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "xeKQrBrnk", - }, - ], - "name": "grafana-default-email", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "provisioned-contact-point", - "provenance": "api", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey@grafana.com", - "singleEmail": false, - }, - "type": "email", - "uid": "s8SdCVjnk", - }, - ], - "name": "provisioned-contact-point", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "lotsa-emails", - "secureFields": {}, - "settings": { - "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", - "message": "{{ template "slack-template" . }}", - "singleEmail": false, - "subject": "some custom value", - }, - "type": "email", - "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", - }, - ], - "name": "lotsa-emails", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "Slack with multiple channels", - "secureFields": { - "token": true, - }, - "settings": { - "recipient": "test-alerts", - }, - "type": "slack", - "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", - }, - { - "disableResolveMessage": false, - "name": "Slack with multiple channels", - "secureFields": { - "token": true, - }, - "settings": { - "recipient": "test-alerts2", - }, - "type": "slack", - "uid": "b286a3be-f690-49e2-8605-b075cbace2df", - }, - ], - "name": "Slack with multiple channels", - }, - { - "grafana_managed_receiver_configs": [ - { - "disableResolveMessage": false, - "name": "Oncall-integration", - "settings": { - "url": "https://oncall-endpoint.example.com", - }, - "type": "oncall", - }, - ], - "name": "OnCall Conctact point", - }, - { - "grafana_managed_receiver_configs": [ + { + "metadata": {}, + "spec": { + "integrations": [ { "disableResolveMessage": false, "name": "my new receiver", - "secureSettings": {}, + "secureFields": {}, "settings": { "addresses": "tester@grafana.com", "singleEmail": false, @@ -141,8 +49,8 @@ exports[`alerting API server disabled should be able to test and save a receiver "type": "email", }, ], - "name": "my new receiver", + "title": "my new receiver", }, - ], + }, ] `; diff --git a/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts b/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts index 4a217a10cac..c92b4f3419c 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/alertManagerSuggestions.ts @@ -1,6 +1,6 @@ import type { Monaco } from '@grafana/ui'; -import { AlertmanagerTemplateFunction } from './language'; +import { AlertmanagerTemplateFunction, GomplateFunctions } from './language'; import { SuggestionDefinition } from './suggestionDefinition'; export function getAlertManagerSuggestions(monaco: Monaco): SuggestionDefinition[] { @@ -50,3 +50,15 @@ export function getAlertManagerSuggestions(monaco: Monaco): SuggestionDefinition }, ]; } + +export function getGomplateSuggestions(monaco: Monaco): SuggestionDefinition[] { + const kind = monaco.languages.CompletionItemKind.Function; + return Object.values(GomplateFunctions).flatMap((functionList) => + functionList.map((func) => ({ + label: func.keyword, + detail: func.usage, + documentation: `${func.definition}\n\n${func.example}`, + kind, + })) + ); +} diff --git a/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts b/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts index edd248d5c52..e41b91db5ac 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/autocomplete.ts @@ -3,7 +3,7 @@ import type { IDisposable, IRange, Position, editor, languages } from 'monaco-ed import type { Monaco } from '@grafana/ui'; -import { getAlertManagerSuggestions } from './alertManagerSuggestions'; +import { getAlertManagerSuggestions, getGomplateSuggestions } from './alertManagerSuggestions'; import { SuggestionDefinition } from './suggestionDefinition'; import { getAlertSuggestions, @@ -49,17 +49,16 @@ export function registerGoTemplateAutocomplete(monaco: Monaco): IDisposable { } function isInsideGoExpression(model: editor.ITextModel, position: Position) { - const searchRange = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: model.getLineMinColumn(position.lineNumber), - endColumn: model.getLineMaxColumn(position.lineNumber), - }; + // Need to trick findMatches into enabling multiline matches. One way to do this is to have \n in the regex. + const goSyntaxRegex = '\\{\\{(?:.|\\n)+?\\}\\}'; + const matches = model.findMatches(goSyntaxRegex, model.getFullModelRange(), true, false, null, false); - const goSyntaxRegex = '\\{\\{[a-zA-Z0-9._() "]+\\}\\}'; - const matches = model.findMatches(goSyntaxRegex, searchRange, true, false, null, true); - - return matches.some((match) => match.range.containsPosition(position)); + return matches.some((match) => + match.range.containsPosition({ + lineNumber: position.lineNumber, + column: position.column + 1, // Stricter check to avoid matching on the closing bracket. + }) + ); } export class CompletionProvider { @@ -73,7 +72,10 @@ export class CompletionProvider { }; getFunctionsSuggestions = (): languages.ProviderResult => { - return this.getCompletionsFromDefinitions(getAlertManagerSuggestions(this.monaco)); + return this.getCompletionsFromDefinitions( + getAlertManagerSuggestions(this.monaco), + getGomplateSuggestions(this.monaco) + ); }; getTemplateDataSuggestions = (wordContext: string): languages.ProviderResult => { diff --git a/public/app/features/alerting/unified/components/receivers/editor/language.ts b/public/app/features/alerting/unified/components/receivers/editor/language.ts index c62e9df4eaa..ec1a5d237ee 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/language.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/language.ts @@ -26,7 +26,83 @@ export enum AlertmanagerTemplateFunction { stringSlice = 'stringSlice', } -export const availableAlertManagerFunctions = Object.values(AlertmanagerTemplateFunction); +// list of available Gomplate functions in Alertmanager templates +// see https://github.com/hairyhenderson/gomplate +export const GomplateFunctions = { + coll: [ + { + keyword: 'coll.Dict', + definition: + 'Creates a map with string keys from key/value pairs. All keys are converted to strings. If an odd number of arguments is provided, the last is used as the key with an empty string value.', + usage: 'function(key string, val any, ...)', + example: `{{ coll.Dict "name" "Frank" "age" 42 | data.ToYAML }}`, + }, + { + keyword: 'coll.Slice', + definition: 'Creates a slice (like an array or list). Useful when needing to range over a bunch of variables.', + usage: 'function(in ...any)', + example: `{{ range coll.Slice "Bart" "Lisa" "Maggie" }}Hello, {{ . }}{{ end }}`, + }, + { + keyword: 'coll.Append', + definition: 'Appends a value to the end of a list. Creates a new list rather than modifying the input.', + usage: 'function(value any, list []any)', + example: `{{ coll.Slice 1 1 2 3 | append 5 }}`, + }, + ], + + data: [ + { + keyword: 'data.JSON', + definition: 'Converts a JSON string into an object. Works for JSON Objects and Arrays.', + usage: 'function(json string)', + example: `{{ ($json | data.JSON).hello }}`, + }, + { + keyword: 'data.ToJSON', + definition: 'Converts an object to a JSON document.', + usage: 'function(obj any)', + example: `{{ (\`{"foo":{"hello":"world"}}\` | json).foo | data.ToJSON }}`, + }, + { + keyword: 'data.ToJSONPretty', + definition: 'Converts an object to a pretty-printed (indented) JSON document.', + usage: 'function(indent string, obj any)', + example: `{{ \`{"hello":"world"}\` | data.JSON | data.ToJSONPretty " " }}`, + }, + ], + + tmpl: [ + { + keyword: 'tmpl.Exec', + definition: + 'Execute (render) the named template. This is equivalent to using the `template` action, except the result is returned as a string. This allows for post-processing of templates.', + usage: 'function(name string, [context any])', + example: `{{ tmpl.Exec "T1" | strings.ToUpper }}`, + }, + { + keyword: 'tmpl.Inline', + definition: + 'Render the given string as a template, just like a nested template. If the template is given a name, it can be re-used later with the `template` keyword. A context can be provided, otherwise the default gomplate context will be used.', + usage: 'function(partial string, context any)', + example: `{{ tmpl.Inline "{{print \`hello world\`}}" }}`, + }, + ], + + time: [ + { + keyword: 'time.Now', + definition: "Returns the current local time, as a time.Time. This wraps Go's time.Now.", + usage: 'function()', + example: `{{ (time.Now).UTC.Format "Day 2 of month 1 in year 2006 (timezone MST)" }}`, + }, + ], +}; + +export const availableAlertManagerFunctions = [ + ...Object.values(AlertmanagerTemplateFunction), + ...Object.keys(GomplateFunctions).map((namespace) => namespace), +]; // boolean functions const booleanFunctions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge']; diff --git a/public/app/features/alerting/unified/components/receivers/editor/snippets.ts b/public/app/features/alerting/unified/components/receivers/editor/snippets.ts index 2230a126977..f6d29dfaa35 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/snippets.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/snippets.ts @@ -28,6 +28,14 @@ Annotations: {{ end }} `; +export const jsonSnippet = ` +{{ coll.Dict + "receiver" .Receiver + "status" .Status + "alerts" ( len .Alerts ) +| data.ToJSONPretty " " }} +`; + export const groupLabelsLoopSnippet = getKeyValueTemplate('GroupLabels.SortedPairs'); export const commonLabelsLoopSnippet = getKeyValueTemplate('CommonLabels.SortedPairs'); export const commonAnnotationsLoopSnippet = getKeyValueTemplate('CommonAnnotations.SortedPairs'); diff --git a/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts b/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts index 6dedd8f9fe3..27c42e5507e 100644 --- a/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts +++ b/public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts @@ -7,6 +7,7 @@ import { commonAnnotationsLoopSnippet, commonLabelsLoopSnippet, groupLabelsLoopSnippet, + jsonSnippet, labelsLoopSnippet, } from './snippets'; import { SuggestionDefinition } from './suggestionDefinition'; @@ -28,6 +29,8 @@ export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] { { label: 'CommonLabels', kind, detail: '[]KeyValue' }, { label: 'CommonAnnotations', kind, detail: '[]KeyValue' }, { label: 'ExternalURL', kind, detail: 'string' }, + { label: 'GroupKey', kind, detail: 'string' }, + { label: 'TruncatedAlerts', kind, detail: 'integer' }, ]; } @@ -104,6 +107,12 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] { detail: 'string', documentation: 'String that contains labels and values of each reduced expression in the alert.', }, + { + label: { label: 'OrgID', detail: '(Alert)' }, + kind, + detail: 'integer', + documentation: 'The ID of the organization that owns the alert.', + }, ]; } @@ -169,6 +178,11 @@ export const snippets = { description: 'Renders a loop through annotations', snippet: annotationsLoopSnippet, }, + json: { + label: 'json', + description: 'Renders a JSON object', + snippet: jsonSnippet, + }, }; // Snippets @@ -176,7 +190,7 @@ export function getSnippetsSuggestions(monaco: Monaco): SuggestionDefinition[] { const snippetKind = monaco.languages.CompletionItemKind.Snippet; const snippetInsertRule = monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; - const { alerts, alertDetails, groupLabels, commonLabels, commonAnnotations, labels, annotations } = snippets; + const { alerts, alertDetails, groupLabels, commonLabels, commonAnnotations, labels, annotations, json } = snippets; return [ { @@ -231,5 +245,12 @@ export function getSnippetsSuggestions(monaco: Monaco): SuggestionDefinition[] { insertText: annotations.snippet, insertTextRules: snippetInsertRule, }, + { + label: json.label, + documentation: json.description, + kind: snippetKind, + insertText: json.snippet, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.KeepWhitespace, + }, ]; } diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx index bd2616efca8..d7bb0f43f2e 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.test.tsx @@ -5,7 +5,6 @@ import { clickSelectOption } from 'test/helpers/selectOptionInTest'; import { render, screen, waitFor } from 'test/test-utils'; import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector'; -import { config } from '@grafana/runtime'; import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure'; import { setOnCallFeatures, @@ -58,49 +57,40 @@ describe('GrafanaReceiverForm', () => { ]); }); - describe('alertingApiServer', () => { - beforeEach(() => { - config.featureToggles.alertingApiServer = true; - }); - afterEach(() => { - config.featureToggles.alertingApiServer = false; + it('handles nested secure fields correctly', async () => { + const capturedRequests = captureRequests( + (req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST' + ); + const { user } = renderWithProvider(); + const { type, click } = user; + + await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument()); + + // Select MQTT receiver and fill out basic required fields for contact point + await clickSelectOption(await byTestId('items.0.type').find(), 'MQTT'); + await type(screen.getByLabelText(/^name/i), 'mqtt contact point'); + await type(screen.getByLabelText(/broker url/i), 'broker url'); + await type(screen.getByLabelText(/topic/i), 'topic'); + + // Fill out fields that we know will be nested secure fields + await click(screen.getByText(/optional mqtt settings/i)); + await click(screen.getByRole('button', { name: /^Add$/i })); + await type(screen.getByLabelText(/ca certificate/i), 'some cert'); + + await click(screen.getByRole('button', { name: /save contact point/i })); + + const [request] = await capturedRequests; + const postRequestbody = await request.clone().json(); + + const integrationPayload = postRequestbody.spec.integrations[0]; + expect(integrationPayload.settings.tlsConfig).toEqual({ + // Expect the payload to have included the value of a secret field + caCertificate: 'some cert', + // And to not have removed other values (which would happen if we incorrectly merged settings together) + insecureSkipVerify: false, }); - it('handles nested secure fields correctly', async () => { - const capturedRequests = captureRequests( - (req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST' - ); - const { user } = renderWithProvider(); - const { type, click } = user; - - await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument()); - - // Select MQTT receiver and fill out basic required fields for contact point - await clickSelectOption(await byTestId('items.0.type').find(), 'MQTT'); - await type(screen.getByLabelText(/^name/i), 'mqtt contact point'); - await type(screen.getByLabelText(/broker url/i), 'broker url'); - await type(screen.getByLabelText(/topic/i), 'topic'); - - // Fill out fields that we know will be nested secure fields - await click(screen.getByText(/optional mqtt settings/i)); - await click(screen.getByRole('button', { name: /^Add$/i })); - await type(screen.getByLabelText(/ca certificate/i), 'some cert'); - - await click(screen.getByRole('button', { name: /save contact point/i })); - - const [request] = await capturedRequests; - const postRequestbody = await request.clone().json(); - - const integrationPayload = postRequestbody.spec.integrations[0]; - expect(integrationPayload.settings.tlsConfig).toEqual({ - // Expect the payload to have included the value of a secret field - caCertificate: 'some cert', - // And to not have removed other values (which would happen if we incorrectly merged settings together) - insecureSkipVerify: false, - }); - - expect(postRequestbody).toMatchSnapshot(); - }); + expect(postRequestbody).toMatchSnapshot(); }); describe('OnCall contact point', () => { diff --git a/public/app/features/alerting/unified/components/receivers/form/__snapshots__/GrafanaReceiverForm.test.tsx.snap b/public/app/features/alerting/unified/components/receivers/form/__snapshots__/GrafanaReceiverForm.test.tsx.snap index 547720877c2..1c0609809b9 100644 --- a/public/app/features/alerting/unified/components/receivers/form/__snapshots__/GrafanaReceiverForm.test.tsx.snap +++ b/public/app/features/alerting/unified/components/receivers/form/__snapshots__/GrafanaReceiverForm.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GrafanaReceiverForm alertingApiServer handles nested secure fields correctly 1`] = ` +exports[`GrafanaReceiverForm handles nested secure fields correctly 1`] = ` { "metadata": {}, "spec": { diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx index 08f6a107ab3..e5d06e8a7b7 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateContentAndPreview.tsx @@ -10,8 +10,9 @@ import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/d import { EditorColumnHeader } from '../../../contact-points/templates/EditorColumnHeader'; import { TemplateEditor } from '../../TemplateEditor'; -import { getPreviewResults } from '../../TemplatePreview'; -import { usePreviewTemplate } from '../../usePreviewTemplate'; +import { TemplatePreview } from '../../TemplatePreview'; + +import { getUseTemplateText } from './utils'; export function TemplateContentAndPreview({ payload, @@ -33,11 +34,6 @@ export function TemplateContentAndPreview({ const { selectedAlertmanager } = useAlertmanager(); const isGrafanaAlertManager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; - const { data, error } = usePreviewTemplate(templateContent, templateName, payload, setPayloadFormatError); - const previewToRender = getPreviewResults(error, payloadFormatError, data); - - const templatePreviewId = 'template-preview'; - return (
    @@ -62,24 +58,15 @@ export function TemplateContentAndPreview({
    {isGrafanaAlertManager && ( -
    - - -
    - {previewToRender} -
    -
    -
    + )}
    ); @@ -98,6 +85,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ borderRadius: theme.shape.radius.default, border: `1px solid ${theme.colors.border.medium}`, }), + templatePreview: css({ + flex: 1, + display: 'flex', + }), + minEditorSize: css({ + minHeight: 300, + minWidth: 300, + }), viewerContainer: ({ height }: { height: number | string }) => css({ height, diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx index 42ac031d381..e8a58495887 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.test.tsx @@ -6,7 +6,6 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { grantUserPermissions } from 'app/features/alerting/unified/mocks'; import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext'; -import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { DEFAULT_TEMPLATES } from 'app/features/alerting/unified/utils/template-constants'; @@ -86,8 +85,6 @@ describe('getTemplateOptions function', () => { }); describe('TemplatesPicker', () => { - testWithFeatureToggles(['alertingApiServer']); - beforeEach(() => { grantUserPermissions([ AccessControlAction.AlertingNotificationsRead, diff --git a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx index dcd2516ebb4..5260695f6c1 100644 --- a/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx @@ -220,9 +220,7 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe /> - copyToClipboard(getUseTemplateText(template?.value?.name ?? defaultTemplateValue?.value?.name ?? '')) - } + onClick={() => copyToClipboard(template?.value?.content ?? defaultTemplateValue?.value?.content ?? '')} name="copy" /> diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx index 116374475da..0a949d9bccb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx @@ -136,18 +136,14 @@ describe('Can create a new grafana managed alert using simplified routing', () = expect(serializedRequests).toMatchSnapshot(); }); - describe('alertingApiServer enabled', () => { - testWithFeatureToggles(['alertingApiServer']); + it('allows selecting a contact point', async () => { + const { user } = renderRuleEditor(); - it('allows selecting a contact point when using alerting API server', async () => { - const { user } = renderRuleEditor(); + await user.click(await ui.inputs.simplifiedRouting.contactPointRouting.find()); - await user.click(await ui.inputs.simplifiedRouting.contactPointRouting.find()); + await selectContactPoint(user, 'Email'); - await selectContactPoint(user, 'Email'); - - expect(await screen.findByText('Email')).toBeInTheDocument(); - }); + expect(await screen.findByText('Email')).toBeInTheDocument(); }); describe('switch modes enabled', () => { diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx index d947711c5c6..fa87aef23a4 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPreview.test.tsx @@ -2,7 +2,6 @@ import { render, screen, waitFor, within } from 'test/test-utils'; import { byRole, byTestId, byText } from 'testing-library-selector'; import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; -import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils'; import { AccessControlAction } from 'app/types/accessControl'; import { MatcherOperator } from '../../../../../../plugins/datasource/alertmanager/types'; @@ -114,13 +113,7 @@ const folder: Folder = { title: 'title', }; -describe.each([ - // k8s API enabled - true, - // k8s API disabled - false, -])('NotificationPreview with alertingApiServer=%p', (apiServerEnabled) => { - apiServerEnabled ? testWithFeatureToggles(['alertingApiServer']) : testWithFeatureToggles([]); +describe('NotificationPreview', () => { jest.retryTimes(2); it('should render notification preview without alert manager label, when having only one alert manager configured to receive alerts', async () => { diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx index 41fdcdbcf3e..a8acf2bdebf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SimpleCondition.tsx @@ -9,7 +9,7 @@ import { Trans, t } from 'app/core/internationalization'; import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { ThresholdSelect } from 'app/features/expressions/components/ThresholdSelect'; import { ExpressionQuery, ExpressionQueryType, reducerTypes, thresholdFunctions } from 'app/features/expressions/types'; -import { getReducerType } from 'app/features/expressions/utils/expressionTypes'; +import { getReducerType, isRangeEvaluator } from 'app/features/expressions/utils/expressionTypes'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { ToLabel } from '../../../../../expressions/components/ToLabel'; @@ -55,9 +55,7 @@ export const SimpleConditionEditor = ({ updateReduceExpression(value.value ?? ReducerID.last, expressionQueriesList, dispatch); }; - const isRange = - simpleCondition.evaluator.type === EvalFunction.IsWithinRange || - simpleCondition.evaluator.type === EvalFunction.IsOutsideRange; + const isRange = isRangeEvaluator(simpleCondition.evaluator.type); const thresholdFunction = thresholdFunctions.find((fn) => fn.value === simpleCondition.evaluator?.type); @@ -71,24 +69,17 @@ export const SimpleConditionEditor = ({ updateThresholdFunction(value.value ?? EvalFunction.IsAbove, expressionQueriesList, dispatch); }; - const onEvaluateValueChange = (event: FormEvent, index?: number) => { - if (isRange) { - const newParams = produce(simpleCondition.evaluator.params, (draft) => { - draft[index ?? 0] = parseFloat(event.currentTarget.value); - }); - // update the condition kept in the parent - onChange({ ...simpleCondition, evaluator: { ...simpleCondition.evaluator, params: newParams } }); - // update the reducer state where we store the queries - updateThresholdValue(parseFloat(event.currentTarget.value), index ?? 0, expressionQueriesList, dispatch); - } else { - // update the condition kept in the parent - onChange({ - ...simpleCondition, - evaluator: { ...simpleCondition.evaluator, params: [parseFloat(event.currentTarget.value)] }, - }); - // update the reducer state where we store the queries - updateThresholdValue(parseFloat(event.currentTarget.value), 0, expressionQueriesList, dispatch); - } + const onEvaluateValueChange = (event: FormEvent, index = 0) => { + const value = event.currentTarget.value; + const numericValue = parseFloat(value) || 0; // try to convert input to a number that isn't NaN + + onChange( + produce(simpleCondition, (draftCondition) => { + draftCondition.evaluator.params[index] = numericValue; + }) + ); + + updateThresholdValue(numericValue, index, expressionQueriesList, dispatch); }; const styles = useStyles2(getStyles); @@ -120,23 +111,35 @@ export const SimpleConditionEditor = ({ onEvaluateValueChange(event, 0)} + // by using the key prop we can force the input to re-render whenever the defaultValue updates + // this is because we have a useEffect() that updates the data structure but "defaultValue" will memoize the + // first value before the useEffect() runs + key={simpleCondition.evaluator.params[0]} + defaultValue={simpleCondition.evaluator.params[0] ?? ''} + onBlur={(event) => { + onEvaluateValueChange(event, 0); + }} /> onEvaluateValueChange(event, 1)} + key={simpleCondition.evaluator.params[1]} + defaultValue={simpleCondition.evaluator.params[1] ?? ''} + onBlur={(event) => { + onEvaluateValueChange(event, 1); + }} /> ) : ( { + onEvaluateValueChange(event, 0); + }} /> )} diff --git a/public/app/features/alerting/unified/components/settings/AlertmanagerCard.tsx b/public/app/features/alerting/unified/components/settings/AlertmanagerCard.tsx index e0aa3eadf64..8499c8161cc 100644 --- a/public/app/features/alerting/unified/components/settings/AlertmanagerCard.tsx +++ b/public/app/features/alerting/unified/components/settings/AlertmanagerCard.tsx @@ -121,7 +121,11 @@ export function AlertmanagerCard({ {/* ⚠️ provisioned Data sources cannot have their "enable" / "disable" actions but we should still allow editing of the configuration */} {showActions ? ( <> diff --git a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx index c629073d589..5c1592ca098 100644 --- a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx +++ b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.test.tsx @@ -47,20 +47,11 @@ describe('Alerting Settings', () => { grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingInstanceRead]); }); - it('should be able to reset alertmanager config', async () => { + it('should not be able to reset alertmanager config', async () => { const onReset = jest.fn(); renderConfiguration('grafana', { onReset }); - await userEvent.click(await ui.resetButton.find()); - - await waitFor(() => { - expect(ui.resetConfirmButton.query()).toBeInTheDocument(); - }); - - await userEvent.click(ui.resetConfirmButton.get()); - - await waitFor(() => expect(onReset).toHaveBeenCalled()); - expect(onReset).toHaveBeenLastCalledWith('grafana'); + expect(ui.resetButton.query()).not.toBeInTheDocument(); }); it('should be able to cancel', async () => { @@ -100,4 +91,20 @@ describe('vanilla Alertmanager', () => { expect(ui.saveButton.get()).toBeInTheDocument(); expect(ui.resetButton.get()).toBeInTheDocument(); }); + + it('should be able to reset non-Grafana alertmanager config', async () => { + const onReset = jest.fn(); + renderConfiguration(PROVISIONED_MIMIR_ALERTMANAGER_UID, { onReset }); + + expect(ui.cancelButton.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeInTheDocument(); + expect(ui.resetButton.get()).toBeInTheDocument(); + + await userEvent.click(ui.resetButton.get()); + + await userEvent.click(ui.resetConfirmButton.get()); + + await waitFor(() => expect(onReset).toHaveBeenCalled()); + expect(onReset).toHaveBeenLastCalledWith(PROVISIONED_MIMIR_ALERTMANAGER_UID); + }); }); diff --git a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx index 05a02bcbec4..bd756fe9241 100644 --- a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx @@ -28,12 +28,12 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave const { loading: isDeleting, error: deletingError } = useUnifiedAlertingSelector((state) => state.deleteAMConfig); const { loading: isSaving, error: savingError } = useUnifiedAlertingSelector((state) => state.saveAMConfig); const [showResetConfirmation, setShowResetConfirmation] = useState(false); + const isGrafanaManagedAlertmanager = alertmanagerName === GRAFANA_RULES_SOURCE_NAME; // ⚠️ provisioned data sources should not prevent the configuration from being edited const immutableDataSource = alertmanagerName ? isVanillaPrometheusAlertManagerDataSource(alertmanagerName) : false; - const readOnly = immutableDataSource; + const readOnly = immutableDataSource || isGrafanaManagedAlertmanager; - const isGrafanaManagedAlertmanager = alertmanagerName === GRAFANA_RULES_SOURCE_NAME; const styles = useStyles2(getStyles); const { @@ -128,12 +128,28 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave ); } - const confirmationText = isGrafanaManagedAlertmanager - ? `Are you sure you want to reset configuration for the Grafana Alertmanager? Contact points and notification policies will be reset to their defaults.` - : `Are you sure you want to reset configuration for "${alertmanagerName}"? Contact points and notification policies will be reset to their defaults.`; + const confirmationText = t( + 'alerting.alertmanager-config.reset-confirmation', + 'Are you sure you want to reset configuration for "{{alertmanagerName}}"? Contact points and notification policies will be reset to their defaults.', + { alertmanagerName } + ); return (
    + {isGrafanaManagedAlertmanager && ( + + + The internal Grafana Alertmanager configuration cannot be manually changed. To change this configuration, + edit the individual resources through the UI. + + + )} {/* form error state */} {errors.configJSON && ( { setDataSourceName(dataSourceName); setOpen(true); @@ -33,14 +37,29 @@ export function useEditConfigurationDrawer() { } const isGrafanaAlertmanager = dataSourceName === GRAFANA_RULES_SOURCE_NAME; - const title = isGrafanaAlertmanager ? 'Internal Grafana Alertmanager' : dataSourceName; + const title = isGrafanaAlertmanager + ? t( + 'alerting.use-edit-configuration-drawer.drawer.internal-grafana-alertmanager-title', + 'Grafana built-in Alertmanager' + ) + : dataSourceName; + + const subtitle = readOnly + ? t( + 'alerting.use-edit-configuration-drawer.drawer.title-view-the-alertmanager-configuration', + 'View Alertmanager configuration' + ) + : t( + 'alerting.use-edit-configuration-drawer.drawer.title-edit-the-alertmanager-configuration', + 'Edit Alertmanager configuration' + ); // @todo check copy return ( ); - }, [open, dataSourceName, handleDismiss, activeTab, updateAlertmanagerSettings, resetAlertmanagerSettings]); + }, [open, dataSourceName, readOnly, handleDismiss, activeTab, updateAlertmanagerSettings, resetAlertmanagerSettings]); return [drawer, showConfiguration, handleDismiss] as const; } diff --git a/public/app/features/alerting/unified/components/settings/InternalAlertmanager.tsx b/public/app/features/alerting/unified/components/settings/InternalAlertmanager.tsx index add2e0f852d..b40619143f3 100644 --- a/public/app/features/alerting/unified/components/settings/InternalAlertmanager.tsx +++ b/public/app/features/alerting/unified/components/settings/InternalAlertmanager.tsx @@ -30,6 +30,7 @@ export default function InternalAlertmanager({ onEditConfiguration }: Props) { onEditConfiguration={handleEditConfiguration} onEnable={handleEnable} onDisable={handleDisable} + readOnly /> ); } diff --git a/public/app/features/alerting/unified/components/settings/VersionManager.tsx b/public/app/features/alerting/unified/components/settings/VersionManager.tsx index b5285d5f990..9a47437094e 100644 --- a/public/app/features/alerting/unified/components/settings/VersionManager.tsx +++ b/public/app/features/alerting/unified/components/settings/VersionManager.tsx @@ -94,11 +94,15 @@ const AlertmanagerConfigurationVersionManager = ({ } if (isLoading) { - return 'Loading...'; + return Loading...; } if (!historicalConfigs.length) { - return 'No previous configurations'; + return ( + + No previous configurations + + ); } // with this function we'll compute the diff with the previous version; that way the user can get some idea of how many lines where changed in each update that was applied diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index 6cd1c875f01..b7efbcd53b5 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -254,3 +254,27 @@ export const makeAllK8sGetEndpointsFail = ( }) ); }; + +export const makeAllK8sEndpointsFail = ( + uid: string, + message = 'could not find an Alertmanager configuration', + status = 500 +) => { + server.use( + http.all(ALERTING_API_SERVER_BASE_URL + '/*', () => { + const errorResponse: ApiMachineryError = { + kind: 'Status', + apiVersion: 'v1', + metadata: {}, + status: 'Failure', + details: { + uid, + }, + message, + code: status, + reason: '', + }; + return HttpResponse.json(errorResponse, { status }); + }) + ); +}; diff --git a/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts b/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts index 84dd2bf1a8c..9e73c77f671 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/accessControl.ts @@ -22,7 +22,7 @@ const resourceDescriptionsMap: Record = { * */ const resourceDetailsMap: Record> = { receivers: { - lotsaEmails: [ + 'lotsa-emails': [ { id: 123, roleName: 'somerole:name', diff --git a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts index b95fc2dcaea..c02659dcab5 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/alertmanagers.ts @@ -142,7 +142,7 @@ const getGrafanaAlertmanagerTemplatePreview = () => const body = await request.json(); if (body?.template.startsWith('{{')) { - return HttpResponse.json({ results: [{ name: 'asdasd', text: `some example preview for ${body.name}` }] }); + return HttpResponse.json({ results: [{ name: 'asdasd', text: `some example preview for ${body.template}` }] }); } return HttpResponse.json({}); diff --git a/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts b/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts index 78b8623956e..a620fe528c2 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/k8s/receivers.k8s.ts @@ -1,12 +1,19 @@ -import { camelCase } from 'lodash'; import { HttpResponse, http } from 'msw'; -import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; +import { + getAlertmanagerConfig, + setAlertmanagerConfig, +} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; +const usedByPolicies = ['grafana-default-email']; +const usedByRules = ['grafana-default-email']; +const cannotBeEdited = ['grafana-default-email']; +const cannotBeDeleted = ['grafana-default-email']; + const getReceiversList = () => { const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); @@ -20,12 +27,14 @@ const getReceiversList = () => { return { metadata: { // This isn't exactly accurate, but its the cleanest way to use the same data for AM config and K8S responses - uid: camelCase(contactPoint.name), + uid: contactPoint.name, annotations: { [K8sAnnotations.Provenance]: provenance, [K8sAnnotations.AccessAdmin]: 'true', - [K8sAnnotations.AccessDelete]: 'true', - [K8sAnnotations.AccessWrite]: 'true', + [K8sAnnotations.AccessDelete]: cannotBeDeleted.includes(contactPoint.name) ? 'false' : 'true', + [K8sAnnotations.AccessWrite]: cannotBeEdited.includes(contactPoint.name) ? 'false' : 'true', + [K8sAnnotations.InUseRoutes]: usedByPolicies.includes(contactPoint.name) ? '1' : '0', + [K8sAnnotations.InUseRules]: usedByRules.includes(contactPoint.name) ? '1' : '0', }, }, spec: { @@ -46,6 +55,35 @@ const listNamespacedReceiverHandler = () => return HttpResponse.json(getReceiversList()); }); +const getNamespacedReceiverHandler = () => + http.get<{ namespace: string; name: string }>( + `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`, + ({ params }) => { + const { name } = params; + const receivers = getReceiversList(); + const matchedReceiver = receivers.items.find((receiver) => receiver.metadata.uid === name); + if (!matchedReceiver) { + return HttpResponse.json({}, { status: 404 }); + } + return HttpResponse.json(matchedReceiver); + } + ); + +const updateNamespacedReceiverHandler = () => + http.put<{ namespace: string; name: string }>( + `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`, + async ({ params, request }) => { + // TODO: Make this update the internal config so API calls "persist" + const { name } = params; + const parsedReceivers = getReceiversList(); + const matchedReceiver = parsedReceivers.items.find((receiver) => receiver.metadata.uid === name); + if (!matchedReceiver) { + return HttpResponse.json({}, { status: 404 }); + } + return HttpResponse.json(parsedReceivers); + } + ); + const createNamespacedReceiverHandler = () => http.post<{ namespace: string }>( `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers`, @@ -60,17 +98,29 @@ const deleteNamespacedReceiverHandler = () => `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers/:name`, ({ params }) => { const { name } = params; - const parsedReceivers = getReceiversList(); - const matchedReceiver = parsedReceivers.items.find((receiver) => receiver.metadata.uid === name); - if (matchedReceiver) { - return HttpResponse.json(parsedReceivers); + const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME); + const matchedReceiver = config.alertmanager_config?.receivers?.find((receiver) => receiver.name === name); + if (!matchedReceiver) { + return HttpResponse.json({}, { status: 404 }); } - return HttpResponse.json({}, { status: 404 }); + + const newConfig = config.alertmanager_config?.receivers?.filter((receiver) => receiver.name !== name); + setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, { + ...config, + alertmanager_config: { + ...config.alertmanager_config, + receivers: newConfig, + }, + }); + const parsedReceivers = getReceiversList(); + return HttpResponse.json(parsedReceivers); } ); const handlers = [ listNamespacedReceiverHandler(), + getNamespacedReceiverHandler(), + updateNamespacedReceiverHandler(), createNamespacedReceiverHandler(), deleteNamespacedReceiverHandler(), ]; diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx index 3671d13d232..059743c99a3 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorCloudOnlyAllowed.test.tsx @@ -29,8 +29,6 @@ jest.mock('../api/ruler', () => ({ fetchRulerRulesNamespace: jest.fn(), })); -// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. -// lets just skip it jest.mock('app/features/query/components/QueryEditorRow', () => ({ // eslint-disable-next-line react/display-name QueryEditorRow: () =>

    hi

    , diff --git a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx index 8e979828aa9..29c1f926dbe 100644 --- a/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/RuleEditorRecordingRule.test.tsx @@ -39,8 +39,6 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) =>
    {actions}
    , })); -// there's no angular scope in test and things go terribly wrong when trying to render the query editor row. -// lets just skip it jest.mock('app/features/query/components/QueryEditorRow', () => ({ // eslint-disable-next-line react/display-name QueryEditorRow: () =>

    hi

    , diff --git a/public/app/features/alerting/unified/utils/k8s/utils.ts b/public/app/features/alerting/unified/utils/k8s/utils.ts index 69444f02b2c..1a99f52b522 100644 --- a/public/app/features/alerting/unified/utils/k8s/utils.ts +++ b/public/app/features/alerting/unified/utils/k8s/utils.ts @@ -1,4 +1,3 @@ -import { config } from '@grafana/runtime'; import { IoK8SApimachineryPkgApisMetaV1ObjectMeta } from 'app/features/alerting/unified/openapi/receiversApi.gen'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; @@ -10,8 +9,7 @@ import { K8sAnnotations, PROVENANCE_NONE } from 'app/features/alerting/unified/u * and the `alertingApiServer` feature toggle being enabled */ export const shouldUseK8sApi = (alertmanager?: string) => { - const featureToggleEnabled = config.featureToggles.alertingApiServer; - return featureToggleEnabled && alertmanager === GRAFANA_RULES_SOURCE_NAME; + return alertmanager === GRAFANA_RULES_SOURCE_NAME; }; type EntityToCheck = { diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index 88bc70a2cb8..13aa9982983 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -293,7 +293,13 @@ export const flattenCombinedRules = (rules: CombinedRuleNamespace[]) => { groups.forEach(({ name: groupName, rules }) => { rules.forEach((rule) => { if (rule.promRule && isAlertingRule(rule.promRule)) { - acc.push({ dataSourceName: getRulesSourceName(rulesSource), namespaceName, groupName, ...rule }); + acc.push({ + dataSourceName: getRulesSourceName(rulesSource), + namespaceName, + groupName, + ...rule, + namespace: { ...rule.namespace, uid: rule.promRule.folderUid }, + }); } }); }); diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index f61e9d94742..f71ce0f1b9e 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -334,8 +334,8 @@ export class ElementState implements LayerElement { placement.height = height; break; case VerticalConstraint.Scale: - placement.top = (relativeTop / (parentContainer?.height ?? height)) * 100; - placement.bottom = (relativeBottom / (parentContainer?.height ?? height)) * 100; + placement.top = (relativeTop / (parentContainer?.height ?? height)) * 100 * transformScale; + placement.bottom = (relativeBottom / (parentContainer?.height ?? height)) * 100 * transformScale; break; } @@ -360,8 +360,8 @@ export class ElementState implements LayerElement { placement.width = width; break; case HorizontalConstraint.Scale: - placement.left = (relativeLeft / (parentContainer?.width ?? width)) * 100; - placement.right = (relativeRight / (parentContainer?.width ?? width)) * 100; + placement.left = (relativeLeft / (parentContainer?.width ?? width)) * 100 * transformScale; + placement.right = (relativeRight / (parentContainer?.width ?? width)) * 100 * transformScale; break; } diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index ca70c8f7c1f..5dd1415a822 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -1,5 +1,6 @@ import { NavModelItem } from '@grafana/data'; import { enrichHelpItem } from 'app/core/components/AppChrome/MegaMenu/utils'; +import { performInviteUserClick, shouldRenderInviteUserButton } from 'app/core/components/InviteUserButton/utils'; import { t } from 'app/core/internationalization'; import { changeTheme } from 'app/core/services/theme'; @@ -99,5 +100,17 @@ export default (navBarTree: NavModelItem[], extensionActions: CommandPaletteActi const navBarActions = navTreeToActions(navBarTree); + if (shouldRenderInviteUserButton) { + navBarActions.push({ + id: 'invite-user', + name: t('navigation.invite-user.invite-new-member-button', 'Invite new member'), + section: t('command-palette.section.actions', 'Actions'), + priority: ACTIONS_PRIORITY, + perform: () => { + performInviteUserClick('command_palette_actions', 'invite-user-command-palette'); + }, + }); + } + return [...globalActions, ...extensionActions, ...navBarActions]; }; diff --git a/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx new file mode 100644 index 00000000000..f6b530d54c8 --- /dev/null +++ b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.test.tsx @@ -0,0 +1,74 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { config } from '@grafana/runtime'; +import { contextSrv } from 'app/core/core'; + +import { AdvisorRedirectNotice } from './AdvisorRedirectNotice'; + +const originalFeatureToggleValue = config.featureToggles.grafanaAdvisor; +jest.mock('@grafana/runtime/internal', () => ({ + UserStorage: jest.fn().mockImplementation(() => ({ + getItem: jest.fn().mockResolvedValue('true'), + setItem: jest.fn().mockResolvedValue(undefined), + })), +})); + +describe('AdvisorRedirectNotice', () => { + beforeEach(() => { + config.featureToggles.grafanaAdvisor = true; + contextSrv.isGrafanaAdmin = true; + }); + + afterEach(() => { + jest.clearAllMocks(); + config.featureToggles.grafanaAdvisor = originalFeatureToggleValue; + }); + + it('should not render when user is not admin', async () => { + contextSrv.isGrafanaAdmin = false; + render(); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + + it('should not render when feature flag is disabled', async () => { + config.featureToggles.grafanaAdvisor = false; + render(); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + it('should render notice with correct content', async () => { + render(); + + await waitFor(() => { + expect(screen.getByRole('status')).toBeInTheDocument(); + expect( + screen.getByText(/Try the new Advisor to uncover potential issues with your data sources and plugins./i) + ).toBeInTheDocument(); + expect(screen.getByText(/Go to Advisor/i)).toBeInTheDocument(); + }); + }); + + it('should dismiss notice when close button is clicked', async () => { + const { rerender } = render(); + + await waitFor(() => { + expect( + screen.getByText(/Try the new Advisor to uncover potential issues with your data sources and plugins./i) + ).toBeInTheDocument(); + }); + + const closeButton = screen.getByRole('button', { name: /close alert/i }); + + await userEvent.click(closeButton); + + await waitFor(() => { + expect( + screen.queryByText(/Try the new Advisor to uncover potential issues with your data sources and plugins./i) + ).not.toBeInTheDocument(); + }); + + // Re-render the component and check that the notice is not rendered + rerender(); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx new file mode 100644 index 00000000000..b83c864d26c --- /dev/null +++ b/public/app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice.tsx @@ -0,0 +1,69 @@ +import { css } from '@emotion/css'; +import { useEffect, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { UserStorage } from '@grafana/runtime/internal'; +import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; +import { contextSrv } from 'app/core/core'; +import { t, Trans } from 'app/core/internationalization'; + +const getStyles = (theme: GrafanaTheme2) => ({ + alertContent: css({ + display: 'flex', + flexDirection: 'row', + padding: 0, + justifyContent: 'space-between', + alignItems: 'center', + }), + alertParagraph: css({ + margin: theme.spacing(0, 1, 0, 0), + lineHeight: theme.spacing(theme.components.height.sm), + }), +}); +const userStorage = new UserStorage('advisor-redirect-notice'); + +export function AdvisorRedirectNotice() { + const styles = useStyles2(getStyles); + const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin; + const canUseAdvisor = hasAdminRights && config.featureToggles.grafanaAdvisor; + const [showNotice, setShowNotice] = useState(false); + useEffect(() => { + if (canUseAdvisor) { + userStorage.getItem('showNotice').then((showNotice) => { + if (showNotice !== 'false') { + setShowNotice(true); + } + }); + } + }, [canUseAdvisor]); + + return showNotice ? ( + { + userStorage.setItem('showNotice', 'false'); + setShowNotice(false); + }} + > +
    +

    + + Try the new Advisor to uncover potential issues with your data sources and plugins. + +

    + + Go to Advisor + +
    +
    + ) : ( + <> + ); +} diff --git a/public/app/features/connections/pages/DataSourcesListPage.tsx b/public/app/features/connections/pages/DataSourcesListPage.tsx index 29f24b7e548..7a794ae3b52 100644 --- a/public/app/features/connections/pages/DataSourcesListPage.tsx +++ b/public/app/features/connections/pages/DataSourcesListPage.tsx @@ -1,4 +1,5 @@ import { Page } from 'app/core/components/Page/Page'; +import { AdvisorRedirectNotice } from 'app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice'; import { DataSourceAddButton } from 'app/features/datasources/components/DataSourceAddButton'; import { DataSourcesList } from 'app/features/datasources/components/DataSourcesList'; import { getDataSourcesCount } from 'app/features/datasources/state'; @@ -11,6 +12,7 @@ export function DataSourcesListPage() { return ( + diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 06673e54ee6..c03102bf873 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -17,7 +17,7 @@ import { } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; -import { isInCloneChain } from '../utils/clone'; +import { containsCloneKey, getOriginalKey, isInCloneChain } from '../utils/clone'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardOutline } from './DashboardOutline'; @@ -101,7 +101,9 @@ export class DashboardEditPane extends SceneObjectBase { return; } - const obj = sceneGraph.findByKey(this, element.id); + const elementId = containsCloneKey(element.id) ? getOriginalKey(element.id) : element.id; + + const obj = sceneGraph.findByKey(this, elementId); if (obj) { this.selectObject(obj, element.id, options); } diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 40f058b6d0e..e8ebbd08d02 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -1,6 +1,6 @@ import { advanceBy } from 'jest-date-mock'; -import { BackendSrv, locationService, setBackendSrv } from '@grafana/runtime'; +import { BackendSrv, config, locationService, setBackendSrv } from '@grafana/runtime'; import { Spec as DashboardV2Spec, defaultSpec as defaultDashboardV2Spec, @@ -26,6 +26,21 @@ import { DASHBOARD_CACHE_TTL, } from './DashboardScenePageStateManager'; +// Mock the config module +jest.mock('@grafana/runtime', () => { + const original = jest.requireActual('@grafana/runtime'); + return { + ...original, + config: { + ...original.config, + featureToggles: { + ...original.config.featureToggles, + dashboardNewLayouts: false, // Default value + }, + }, + }; +}); + jest.mock('app/features/dashboard/api/dashboard_api', () => ({ getDashboardAPI: jest.fn(), })); @@ -776,6 +791,7 @@ describe('DashboardScenePageStateManager v2', () => { describe('UnifiedDashboardScenePageStateManager', () => { afterEach(() => { store.delete(DASHBOARD_FROM_LS_KEY); + config.featureToggles.dashboardNewLayouts = false; }); describe('when fetching/loading a dashboard', () => { @@ -987,6 +1003,86 @@ describe('UnifiedDashboardScenePageStateManager', () => { expect(loader.state.dashboard!.serializer.initialSaveModel).toEqual(customHomeDashboardV1Spec); }); }); + + describe('Provisioned dashboard', () => { + it('should load a provisioned v1 dashboard', async () => { + const loader = new UnifiedDashboardScenePageStateManager({}); + setBackendSrv({ + get: () => Promise.resolve(v1ProvisionedDashboardResource), + } as unknown as BackendSrv); + await loader.loadDashboard({ uid: 'blah-blah', route: DashboardRoutes.Provisioning }); + + expect(loader.state.dashboard).toBeDefined(); + expect(loader.state.dashboard!.serializer.initialSaveModel).toEqual( + v1ProvisionedDashboardResource.resource.dryRun.spec + ); + }); + + it('should load a provisioned v2 dashboard', async () => { + const loader = new UnifiedDashboardScenePageStateManager({}); + setBackendSrv({ + get: () => Promise.resolve(v2ProvisionedDashboardResource), + } as unknown as BackendSrv); + await loader.loadDashboard({ uid: 'blah-blah', route: DashboardRoutes.Provisioning }); + + expect(loader.state.dashboard).toBeDefined(); + expect(loader.state.dashboard!.serializer.initialSaveModel).toEqual( + v2ProvisionedDashboardResource.resource.dryRun.spec + ); + }); + }); + + describe('New dashboards', () => { + it('should use v1 manager for new dashboards when dashboardNewLayouts feature toggle is disabled', async () => { + config.featureToggles.dashboardNewLayouts = false; + + const manager = new UnifiedDashboardScenePageStateManager({}); + manager.setActiveManager('v2'); + expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2); + + await manager.loadDashboard({ uid: '', route: DashboardRoutes.New }); + + expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManager); + expect(manager.state.dashboard).toBeDefined(); + expect(manager.state.dashboard?.state.title).toBe('New dashboard'); + }); + + it('should use v2 manager for new dashboards when dashboardNewLayouts feature toggle is enabled', async () => { + config.featureToggles.dashboardNewLayouts = true; + + const manager = new UnifiedDashboardScenePageStateManager({}); + manager.setActiveManager('v1'); + expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManager); + + await manager.loadDashboard({ uid: '', route: DashboardRoutes.New }); + + expect(manager['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2); + expect(manager.state.dashboard).toBeDefined(); + expect(manager.state.dashboard?.state.title).toBe('New dashboard'); + }); + + it('should maintain manager version for subsequent loads based on feature toggle', async () => { + config.featureToggles.dashboardNewLayouts = false; + const manager1 = new UnifiedDashboardScenePageStateManager({}); + manager1.setActiveManager('v2'); + await manager1.loadDashboard({ uid: '', route: DashboardRoutes.New }); + expect(manager1['activeManager']).toBeInstanceOf(DashboardScenePageStateManager); + + manager1.setActiveManager('v2'); + await manager1.loadDashboard({ uid: '', route: DashboardRoutes.New }); + expect(manager1['activeManager']).toBeInstanceOf(DashboardScenePageStateManager); + + config.featureToggles.dashboardNewLayouts = true; + const manager2 = new UnifiedDashboardScenePageStateManager({}); + manager2.setActiveManager('v1'); + await manager2.loadDashboard({ uid: '', route: DashboardRoutes.New }); + expect(manager2['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2); + + manager2.setActiveManager('v1'); + await manager2.loadDashboard({ uid: '', route: DashboardRoutes.New }); + expect(manager2['activeManager']).toBeInstanceOf(DashboardScenePageStateManagerV2); + }); + }); }); const customHomeDashboardV1Spec = { @@ -1116,3 +1212,446 @@ const customHomeDashboardV2Spec = { }, }, }; + +const v1ProvisionedDashboardResource = { + kind: 'ResourceWrapper', + apiVersion: 'provisioning.grafana.app/v0alpha1', + path: 'new-dashboard.json', + ref: 'dashboard/2025-04-11-nkXIe', + hash: '28e6dd34e226ec27e19f9894270290fc105a77b0', + repository: { + type: 'github', + title: 'https://github.com/dprokop/grafana-git-sync-test', + namespace: 'default', + name: 'repository-643e5fb', + }, + urls: { + sourceURL: 'https://github.com/dprokop/grafana-git-sync-test/blob/dashboard/2025-04-11-nkXIe/new-dashboard.json', + repositoryURL: 'https://github.com/dprokop/grafana-git-sync-test', + newPullRequestURL: + 'https://github.com/dprokop/grafana-git-sync-test/compare/main...dashboard/2025-04-11-nkXIe?quick_pull=1&labels=grafana', + compareURL: 'https://github.com/dprokop/grafana-git-sync-test/compare/main...dashboard/2025-04-11-nkXIe', + }, + resource: { + type: { + group: 'dashboard.grafana.app', + version: 'v1alpha1', + kind: 'Dashboard', + resource: 'dashboards', + }, + file: {}, + existing: {}, + action: 'update', + dryRun: { + apiVersion: 'dashboard.grafana.app/v1alpha1', + kind: 'Dashboard', + metadata: { + annotations: { + 'grafana.app/managedBy': 'repo', + 'grafana.app/managerId': 'repository-643e5fb', + 'grafana.app/sourceChecksum': '28e6dd34e226ec27e19f9894270290fc105a77b0', + 'grafana.app/sourcePath': 'new-dashboard.json', + }, + creationTimestamp: '2025-04-09T07:27:46Z', + generation: 1, + managedFields: [ + { + apiVersion: 'dashboard.grafana.app/v1alpha1', + fieldsType: 'FieldsV1', + fieldsV1: { + 'f:metadata': { + 'f:annotations': { + '.': {}, + 'f:grafana.app/managedBy': {}, + 'f:grafana.app/managerId': {}, + 'f:grafana.app/sourceChecksum': {}, + 'f:grafana.app/sourcePath': {}, + }, + }, + 'f:spec': { + 'f:annotations': { + '.': {}, + 'f:list': {}, + }, + 'f:editable': {}, + 'f:fiscalYearStartMonth': {}, + 'f:graphTooltip': {}, + 'f:links': {}, + 'f:panels': {}, + 'f:preload': {}, + 'f:schemaVersion': {}, + 'f:tags': {}, + 'f:templating': { + '.': {}, + 'f:list': {}, + }, + 'f:time': { + '.': {}, + 'f:from': {}, + 'f:to': {}, + }, + 'f:timepicker': {}, + 'f:timezone': {}, + 'f:title': {}, + }, + }, + manager: 'grafana', + operation: 'Update', + time: '2025-04-11T10:35:05Z', + }, + ], + name: 'adsm7zf', + namespace: 'default', + resourceVersion: '1744183666927980', + uid: 'ef523e2b-1e66-4921-b3f9-e7a9b215c988', + }, + spec: { + annotations: { + list: [ + { + builtIn: 1, + datasource: { + type: 'grafana', + uid: '-- Grafana --', + }, + enable: true, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + type: 'dashboard', + }, + ], + }, + editable: true, + fiscalYearStartMonth: 0, + graphTooltip: 0, + links: [], + panels: [ + { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'PD8C576611E62080A', + }, + 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', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + gridPos: { + h: 8, + w: 10, + x: 0, + y: 0, + }, + id: 1, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + hideZeros: false, + mode: 'single', + sort: 'none', + }, + }, + pluginVersion: '12.0.0-pre', + targets: [ + { + refId: 'A', + }, + ], + title: 'New panel', + type: 'timeseries', + }, + ], + preload: false, + schemaVersion: 41, + tags: [], + templating: { + list: [], + }, + time: { + from: 'now-6h', + to: 'now', + }, + timepicker: {}, + timezone: 'browser', + title: 'New dashboard', + }, + status: {}, + }, + upsert: null, + }, +}; + +const v2ProvisionedDashboardResource = { + kind: 'ResourceWrapper', + apiVersion: 'provisioning.grafana.app/v0alpha1', + path: 'v2dashboards/new-dashboard-2025-04-09-nTqgq.json', + ref: 'dashboard/2025-04-11-FzFKZ', + hash: '2d1c7981d4327f5c75afd920e910f1f82e9be706', + repository: { + type: 'github', + title: 'https://github.com/dprokop/grafana-git-sync-test', + namespace: 'default', + name: 'repository-643e5fb', + }, + urls: { + sourceURL: + 'https://github.com/dprokop/grafana-git-sync-test/blob/dashboard/2025-04-11-FzFKZ/v2dashboards/new-dashboard-2025-04-09-nTqgq.json', + repositoryURL: 'https://github.com/dprokop/grafana-git-sync-test', + newPullRequestURL: + 'https://github.com/dprokop/grafana-git-sync-test/compare/main...dashboard/2025-04-11-FzFKZ?quick_pull=1&labels=grafana', + compareURL: 'https://github.com/dprokop/grafana-git-sync-test/compare/main...dashboard/2025-04-11-FzFKZ', + }, + resource: { + type: { + group: 'dashboard.grafana.app', + version: 'v2alpha1', + kind: 'Dashboard', + resource: 'dashboards', + }, + file: {}, + existing: {}, + action: 'update', + dryRun: { + apiVersion: 'dashboard.grafana.app/v2alpha1', + kind: 'Dashboard', + metadata: { + annotations: { + 'grafana.app/folder': 'v2dashboards-8mdocprxtfyldpbpod3ayidiitt', + 'grafana.app/managedBy': 'repo', + 'grafana.app/managerId': 'repository-643e5fb', + 'grafana.app/sourceChecksum': '2d1c7981d4327f5c75afd920e910f1f82e9be706', + 'grafana.app/sourcePath': 'v2dashboards/new-dashboard-2025-04-09-nTqgq.json', + }, + creationTimestamp: '2025-04-09T12:11:20Z', + generation: 1, + name: 'dfeidsuico01kwc', + namespace: 'default', + resourceVersion: '1744200680060000', + uid: '47ed4201-a181-4d1c-b755-17548526d294', + }, + spec: { + annotations: [ + { + kind: 'AnnotationQuery', + spec: { + builtIn: true, + datasource: { + type: 'grafana', + uid: '-- Grafana --', + }, + enable: true, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + }, + }, + ], + cursorSync: 'Off', + description: '', + editable: true, + elements: { + 'panel-1': { + kind: 'Panel', + spec: { + data: { + kind: 'QueryGroup', + spec: { + queries: [ + { + kind: 'PanelQuery', + spec: { + datasource: { + type: 'grafana-testdata-datasource', + uid: 'PD8C576611E62080A', + }, + hidden: false, + query: { + kind: 'grafana-testdata-datasource', + spec: { + scenarioId: 'random_walk', + seriesCount: 2, + }, + }, + refId: 'A', + }, + }, + ], + queryOptions: {}, + transformations: [], + }, + }, + description: '', + id: 1, + links: [], + title: 'New panel', + vizConfig: { + kind: 'timeseries', + spec: { + fieldConfig: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisBorderShow: false, + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + barWidthFactor: 0.6, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + insertNulls: false, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: 0, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + hideZeros: false, + mode: 'single', + sort: 'none', + }, + }, + pluginVersion: '12.0.0-pre', + }, + }, + }, + }, + }, + layout: { + kind: 'AutoGridLayout', + spec: { + columnWidthMode: 'standard', + items: [ + { + kind: 'AutoGridLayoutItem', + spec: { + element: { + kind: 'ElementReference', + name: 'panel-1', + }, + }, + }, + ], + maxColumnCount: 3, + rowHeightMode: 'short', + }, + }, + links: [], + liveNow: false, + preload: false, + tags: [], + timeSettings: { + autoRefresh: '', + autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], + fiscalYearStartMonth: 0, + from: 'now-6h', + hideTimepicker: false, + timezone: 'browser', + to: 'now', + }, + title: 'v2 test - auto grid', + variables: [], + }, + status: {}, + }, + upsert: null, + }, +}; diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 33b8661681d..438c8d5cd73 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -4,10 +4,16 @@ import { locationUtil, UrlQueryMap } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { sceneGraph } from '@grafana/scenes'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { BASE_URL } from 'app/api/clients/provisioning/baseAPI'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors'; import { startMeasure, stopMeasure } from 'app/core/utils/metrics'; -import { AnnoKeyFolder } from 'app/features/apiserver/types'; +import { + AnnoKeyFolder, + AnnoKeyManagerIdentity, + AnnoKeyManagerKind, + AnnoKeySourcePath, +} from 'app/features/apiserver/types'; import { transformDashboardV2SpecToV1 } from 'app/features/dashboard/api/ResponseTransformers'; import { DashboardVersionError, DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { isDashboardV2Resource, isDashboardV2Spec } from 'app/features/dashboard/api/utils'; @@ -15,6 +21,7 @@ import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor'; import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking'; +import { ProvisioningPreview } from 'app/features/provisioning/types'; import { DashboardDataDTO, DashboardDTO, @@ -176,6 +183,84 @@ abstract class DashboardScenePageStateManagerBase } } + protected async loadProvisioningDashboard(repo: string, path: string): Promise { + const params = new URLSearchParams(window.location.search); + const ref = params.get('ref') ?? undefined; // commit hash or branch + + const url = `${BASE_URL}/repositories/${repo}/files/${path}`; + return getBackendSrv() + .get(url, ref ? { ref } : undefined) + .then((v) => { + // Load the results from dryRun + const dryRun = v.resource.dryRun; + if (!dryRun) { + return Promise.reject('failed to read provisioned dashboard'); + } + + if (!dryRun.apiVersion.startsWith('dashboard.grafana.app')) { + return Promise.reject('unexpected resource type: ' + dryRun.apiVersion); + } + + return this.processDashboardFromProvisioning(repo, path, dryRun, { + file: url, + ref: ref, + repo: repo, + }); + }); + } + + private processDashboardFromProvisioning( + repo: string, + path: string, + dryRun: any, + provisioningPreview: ProvisioningPreview + ) { + if (dryRun.apiVersion.split('/')[1] === 'v2alpha1') { + return { + ...dryRun, + kind: 'DashboardWithAccessInfo', + access: { + canStar: false, + isSnapshot: false, + canShare: false, + + // Should come from the repo settings + canDelete: true, + canSave: true, + canEdit: true, + }, + }; + } + + let anno = dryRun.metadata.annotations; + if (!anno) { + dryRun.metadata.annotations = {}; + } + anno[AnnoKeyManagerKind] = 'repo'; + anno[AnnoKeyManagerIdentity] = repo; + anno[AnnoKeySourcePath] = provisioningPreview.ref ? path + '#' + provisioningPreview.ref : path; + + return { + meta: { + canStar: false, + isSnapshot: false, + canShare: false, + + // Should come from the repo settings + canDelete: true, + canSave: true, + canEdit: true, + + // Includes additional k8s metadata + k8s: dryRun.metadata, + + // lookup info + provisioning: provisioningPreview, + }, + dashboard: dryRun.spec, + }; + } + public async loadDashboard(options: LoadDashboardOptions) { try { startMeasure(LOAD_SCENE_MEASUREMENT); @@ -230,15 +315,15 @@ abstract class DashboardScenePageStateManagerBase // Handling home dashboard flow separately from regular dashboard flow. if (options.route === DashboardRoutes.Home) { return await this.loadHomeDashboard(); - } else { - const rsp = await this.fetchDashboard(options); - - if (!rsp) { - return null; - } - - return this.transformResponseToScene(rsp, options); } + + const rsp = await this.fetchDashboard(options); + + if (!rsp) { + return null; + } + + return this.transformResponseToScene(rsp, options); } public getDashboardFromCache(cacheKey: string): T | null { @@ -356,9 +441,8 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag case DashboardRoutes.New: rsp = await buildNewDashboardSaveModel(urlFolderUid); break; - case DashboardRoutes.Provisioning: { - return await dashboardLoaderSrv.loadDashboard('provisioning', slug, uid); - } + case DashboardRoutes.Provisioning: + return this.loadProvisioningDashboard(slug || '', uid); case DashboardRoutes.Public: { return await dashboardLoaderSrv.loadDashboard('public', '', uid); } @@ -537,6 +621,9 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan case DashboardRoutes.New: rsp = await buildNewDashboardSaveModelV2(urlFolderUid); break; + case DashboardRoutes.Provisioning: { + return await this.loadProvisioningDashboard(slug || '', uid); + } case DashboardRoutes.Public: { return await this.dashboardLoader.loadDashboard('public', '', uid); } @@ -659,7 +746,6 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta this.v1Manager = new DashboardScenePageStateManager(initialState); this.v2Manager = new DashboardScenePageStateManagerV2(initialState); - // Start with v2 if newDashboardLayout is enabled, otherwise v1 this.activeManager = this.v1Manager; } @@ -704,7 +790,6 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta if (!rsp) { return null; } - if (isDashboardV2Resource(rsp)) { this.activeManager = this.v2Manager; return this.v2Manager.transformResponseToScene(rsp, options); @@ -752,8 +837,20 @@ export class UnifiedDashboardScenePageStateManager extends DashboardScenePageSta } public async loadDashboard(options: LoadDashboardOptions): Promise { + if (options.route === DashboardRoutes.New) { + const newDashboardVersion = config.featureToggles.dashboardNewLayouts ? 'v2' : 'v1'; + this.setActiveManager(newDashboardVersion); + } return this.withVersionHandling((manager) => manager.loadDashboard.call(this, options)); } + + public setActiveManager(manager: 'v1' | 'v2') { + if (manager === 'v1') { + this.activeManager = this.v1Manager; + } else { + this.activeManager = this.v2Manager; + } + } } const managers: { diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 4c476384f8e..2a6d6abece9 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -29,6 +29,7 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel, ScopeMeta } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { DashboardJson } from 'app/features/manage-dashboards/types'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; @@ -39,7 +40,11 @@ import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; import { DashboardChangeInfo } from '../saving/shared'; -import { DashboardSceneSerializerLike, getDashboardSceneSerializer } from '../serialization/DashboardSceneSerializer'; +import { + DashboardSceneSerializerLike, + getDashboardSceneSerializer, + V2DashboardSerializer, +} from '../serialization/DashboardSceneSerializer'; import { serializeAutoGridItem } from '../serialization/layoutSerializers/AutoGridLayoutSerializer'; import { gridItemToGridLayoutItemKind } from '../serialization/layoutSerializers/DefaultGridLayoutSerializer'; import { getElement } from '../serialization/layoutSerializers/utils'; @@ -177,7 +182,9 @@ export class DashboardScene extends SceneObjectBase impleme public serializer: DashboardSceneSerializerLike< Dashboard | DashboardV2Spec, - DashboardMeta | DashboardWithAccessInfo['metadata'] + DashboardMeta | DashboardWithAccessInfo['metadata'], + Dashboard | DashboardV2Spec, + DashboardJson | DashboardV2Spec >; private _layoutRestorer = new LayoutRestorer(); @@ -193,7 +200,7 @@ export class DashboardScene extends SceneObjectBase impleme ...state, editPane: new DashboardEditPane(), scopesBridge: config.featureToggles.scopeFilters ? new SceneScopesBridge({}) : undefined, - layoutOrchestrator: config.featureToggles.dashboardNewLayouts ? new DashboardLayoutOrchestrator() : undefined, + layoutOrchestrator: new DashboardLayoutOrchestrator(), }); this.serializer = @@ -767,8 +774,10 @@ export class DashboardScene extends SceneObjectBase impleme getSaveResource(options: SaveDashboardAsOptions): ResourceForCreate { const { meta } = this.state; const spec = this.getSaveAsModel(options); + + const apiVersion = this.serializer instanceof V2DashboardSerializer ? 'v2alpha1' : 'v1alpha1'; // get from the dashboard? return { - apiVersion: 'dashboard.grafana.app/v1alpha1', // get from the dashboard? + apiVersion: `dashboard.grafana.app/${apiVersion}`, kind: 'Dashboard', metadata: { ...meta.k8s, diff --git a/public/app/features/dashboard-scene/scene/export/exporters.test.ts b/public/app/features/dashboard-scene/scene/export/exporters.test.ts new file mode 100644 index 00000000000..4f93d54cf39 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/export/exporters.test.ts @@ -0,0 +1,656 @@ +import { find } from 'lodash'; + +import { DataSourceInstanceSettings, DataSourceRef, PanelPluginMeta, TypedVariableModel } from '@grafana/data'; +import { Dashboard, DashboardCursorSync, ThresholdsMode } from '@grafana/schema'; +import { handyTestingSchema } from '@grafana/schema/dist/esm/schema/dashboard/v2_examples'; +import { + DatasourceVariableKind, + QueryVariableKind, +} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import config from 'app/core/config'; +import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; + +import { LibraryElementKind } from '../../../library-panels/types'; +import { DashboardJson } from '../../../manage-dashboards/types'; +import { variableAdapters } from '../../../variables/adapters'; +import { createConstantVariableAdapter } from '../../../variables/constant/adapter'; +import { createDataSourceVariableAdapter } from '../../../variables/datasource/adapter'; +import { createQueryVariableAdapter } from '../../../variables/query/adapter'; + +import { makeExportableV1, makeExportableV2, LibraryElementExport } from './exporters'; + +jest.mock('app/core/store', () => { + return { + getBool: jest.fn(), + getObject: jest.fn((_a, b) => b), + get: jest.fn(), + }; +}); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getDataSourceSrv: () => { + return { + get: (v: string | DataSourceRef) => { + const s = getStubInstanceSettings(v); + return Promise.resolve(s); + }, + getInstanceSettings: getStubInstanceSettings, + }; + }, + config: { + buildInfo: {}, + panels: {}, + apps: {}, + featureToggles: { + newVariables: false, + }, + }, +})); + +jest.mock('app/features/library-panels/state/api', () => ({ + getLibraryPanel: jest.fn().mockReturnValue( + Promise.resolve({ + name: 'Testing lib panel 1', + uid: 'abc-123', + model: { + type: 'graph', + datasource: { + type: 'testdb', + uid: '${DS_GFDB}', + }, + }, + }) + ), +})); + +variableAdapters.register(createQueryVariableAdapter()); +variableAdapters.register(createConstantVariableAdapter()); +variableAdapters.register(createDataSourceVariableAdapter()); + +describe('dashboard exporter v1', () => { + it('handles a default datasource in a template variable', async () => { + const dashboard: any = { + templating: { + list: [ + { + current: {}, + definition: 'test', + error: {}, + hide: 0, + includeAll: false, + multi: false, + name: 'query0', + options: [], + query: { + query: 'test', + refId: 'StandardVariableQuery', + }, + refresh: 1, + regex: '', + skipUrlSync: false, + sort: 0, + type: 'query', + }, + ], + }, + }; + const dashboardModel = new DashboardModel(dashboard, undefined, { + getVariablesFromState: () => dashboard.templating.list, + }); + + const exported: any = await makeExportableV1(dashboardModel); + expect(exported.templating.list[0].datasource.uid).toBe('${DS_GFDB}'); + }); + + it('do not expose datasource name and id in a in a template variable of type datasource', async () => { + const dashboard: Dashboard = { + title: 'My dashboard', + revision: 1, + editable: false, + graphTooltip: DashboardCursorSync.Off, + schemaVersion: 1, + timepicker: { hidden: true }, + timezone: '', + panels: [ + { + id: 1, + type: 'timeseries', + title: 'My panel title', + gridPos: { x: 0, y: 0, w: 1, h: 1 }, + }, + ], + templating: { + list: [ + { + current: { + selected: false, + text: 'my-prometheus-datasource', + value: 'my-prometheus-datasource-uid', + }, + hide: 0, + includeAll: false, + multi: false, + name: 'query1', + options: [], + query: 'prometheus', + refresh: 1, + regex: '', + skipUrlSync: false, + type: 'datasource', + }, + ], + }, + }; + const dashboardModel = new DashboardModel(dashboard, undefined, { + getVariablesFromState: () => dashboard.templating!.list! as TypedVariableModel[], + }); + const exported = (await makeExportableV1(dashboardModel)) as DashboardJson; + const value = exported?.templating?.list ? exported?.templating?.list[0].current : ''; + expect(value).toEqual({}); + }); + + it('replaces datasource ref in library panel', async () => { + const dashboard: Dashboard = { + editable: true, + graphTooltip: 1, + schemaVersion: 38, + panels: [ + { + id: 1, + title: 'Panel title', + type: 'timeseries', + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + transformations: [], + transparent: false, + fieldConfig: { + defaults: { + custom: { + align: 'auto', + cellOptions: { + type: 'auto', + }, + inspect: false, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: 10, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + gridPos: { + h: 8, + w: 12, + x: 0, + y: 0, + }, + libraryPanel: { + name: 'Testing lib panel', + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + }, + }, + ], + }; + + const dashboardModel = new DashboardModel(dashboard, {}); + + const exported = (await makeExportableV1(dashboardModel)) as DashboardJson; + if ('error' in exported) { + throw new Error('error should not be returned when making exportable json'); + } + expect(exported.__elements!['c46a6b49-de40-43b3-982c-1b5e1ec084a4'].model.datasource.uid).toBe('${DS_GFDB}'); + expect(exported.__inputs![0].name).toBe('DS_GFDB'); + }); + + it('If a panel queries has no datasource prop ignore it', async () => { + const dashboard = { + panels: [ + { + id: 1, + type: 'graph', + datasource: { + uid: 'other', + type: 'other', + }, + targets: [{ refId: 'A', a: 'A' }], + }, + ], + } as unknown as Dashboard; + const dashboardModel = new DashboardModel(dashboard, undefined, { + getVariablesFromState: () => [], + }); + const exported: any = await makeExportableV1(dashboardModel); + expect(exported.panels[0].datasource).toEqual({ uid: '${DS_OTHER}', type: 'other' }); + expect(exported.panels[0].targets[0].datasource).toEqual({ uid: '${DS_OTHER}', type: 'other' }); + }); + + describe('given dashboard with repeated panels', () => { + let dash: any, exported: any; + + beforeEach((done) => { + dash = { + templating: { + list: [ + { + name: 'apps', + type: 'query', + datasource: { uid: 'gfdb', type: 'testdb' }, + current: { value: 'Asd', text: 'Asd' }, + options: [{ value: 'Asd', text: 'Asd' }], + }, + { + name: 'prefix', + type: 'constant', + current: { value: 'collectd', text: 'collectd' }, + options: [], + query: 'collectd', + }, + { + name: 'ds', + type: 'datasource', + query: 'other2', + current: { value: 'other2', text: 'other2' }, + options: [], + }, + ], + }, + annotations: { + list: [ + { + name: 'logs', + datasource: 'gfdb', + }, + ], + }, + panels: [ + { id: 6, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'graph' }, + { id: 7 }, + { + id: 8, + datasource: { uid: '-- Mixed --', type: 'mixed' }, + targets: [{ datasource: { uid: 'other', type: 'other' } }], + }, + { id: 9, datasource: { uid: '$ds', type: 'other2' } }, + { + id: 17, + libraryPanel: { + name: 'Library Panel 2', + uid: 'ah8NqyDPs', + }, + }, + { + id: 2, + repeat: 'apps', + datasource: { uid: 'gfdb', type: 'testdb' }, + type: 'graph', + }, + { id: 3, repeat: null, repeatPanelId: 2 }, + { + id: 4, + collapsed: true, + panels: [ + { id: 10, datasource: { uid: 'gfdb', type: 'testdb' }, type: 'table' }, + { id: 11 }, + { + id: 12, + datasource: { uid: '-- Mixed --', type: 'mixed' }, + targets: [{ datasource: { uid: 'other', type: 'other' } }], + }, + { id: 13, datasource: { uid: '$uid', type: 'other' } }, + { + id: 14, + repeat: 'apps', + datasource: { uid: 'gfdb', type: 'testdb' }, + type: 'heatmap', + }, + { id: 15, repeat: null, repeatPanelId: 14 }, + { + id: 16, + datasource: { uid: 'gfdb', type: 'testdb' }, + type: 'graph', + libraryPanel: { + name: 'Library Panel', + uid: 'jL6MrxCMz', + }, + }, + ], + }, + { + id: 5, + targets: [{ scenarioId: 'random_walk', refId: 'A' }], + }, + ], + }; + + config.buildInfo.version = '3.0.2'; + + config.panels['graph'] = { + id: 'graph', + name: 'Graph', + info: { version: '1.1.0' }, + } as PanelPluginMeta; + + config.panels['table'] = { + id: 'table', + name: 'Table', + info: { version: '1.1.1' }, + } as PanelPluginMeta; + + config.panels['heatmap'] = { + id: 'heatmap', + name: 'Heatmap', + info: { version: '1.1.2' }, + } as PanelPluginMeta; + + dash = new DashboardModel( + dash, + {}, + { + getVariablesFromState: () => dash.templating.list, + } + ); + + // init library panels + dash.getPanelById(17).initLibraryPanel({ + uid: 'ah8NqyDPs', + name: 'Library Panel 2', + model: { + datasource: { type: 'other2', uid: '$ds' }, + targets: [{ refId: 'A', datasource: { type: 'other2', uid: '$ds' } }], + type: 'graph', + }, + }); + + makeExportableV1(dash).then((clean) => { + exported = clean; + done(); + }); + }); + + it('should replace datasource refs', () => { + const panel = exported.panels[0]; + expect(panel.datasource.uid).toBe('${DS_GFDB}'); + }); + + it('should explicitly specify default datasources', () => { + const panel = exported.panels[7]; + expect(exported.__inputs.some((ds: Record) => ds.name === 'DS_GFDB')).toBeTruthy(); + expect(panel.datasource.uid).toBe('${DS_GFDB}'); + expect(panel.targets[0].datasource).toEqual({ type: 'testdb', uid: '${DS_GFDB}' }); + }); + + it('should not include default datasource in __inputs unnecessarily', async () => { + const testJson = { + panels: [{ id: 1, datasource: { uid: 'other', type: 'other' }, type: 'graph' }], + } as unknown as Dashboard; + const testDash = new DashboardModel(testJson); + const exportedJson: any = await makeExportableV1(testDash); + expect(exportedJson.__inputs.some((ds: Record) => ds.name === 'DS_GFDB')).toBeFalsy(); + }); + + it('should replace datasource refs in collapsed row', () => { + const panel = exported.panels[6].panels[0]; + expect(panel.datasource.uid).toBe('${DS_GFDB}'); + }); + + it('should replace datasource in variable query', () => { + expect(exported.templating.list[0].datasource.uid).toBe('${DS_GFDB}'); + expect(exported.templating.list[0].options.length).toBe(0); + expect(exported.templating.list[0].current.value).toBe(undefined); + expect(exported.templating.list[0].current.text).toBe(undefined); + }); + + it('should replace datasource in annotation query', () => { + expect(exported.annotations.list[1].datasource.uid).toBe('${DS_GFDB}'); + }); + + it('should add datasource as input', () => { + expect(exported.__inputs[0].name).toBe('DS_GFDB'); + expect(exported.__inputs[0].pluginId).toBe('testdb'); + expect(exported.__inputs[0].type).toBe('datasource'); + }); + + it('should add datasource to required', () => { + const require = find(exported.__requires, { name: 'TestDB' }); + expect(require.name).toBe('TestDB'); + expect(require.id).toBe('testdb'); + expect(require.type).toBe('datasource'); + expect(require.version).toBe('1.2.1'); + }); + + it('should not add built in datasources to required', () => { + const require = find(exported.__requires, { name: 'Mixed' }); + expect(require).toBe(undefined); + }); + + it('should add datasources used in mixed mode', () => { + const require = find(exported.__requires, { name: 'OtherDB' }); + expect(require).not.toBe(undefined); + }); + + it('should add graph panel to required', () => { + const require = find(exported.__requires, { name: 'Graph' }); + expect(require.name).toBe('Graph'); + expect(require.id).toBe('graph'); + expect(require.version).toBe('1.1.0'); + }); + + it('should add table panel to required', () => { + const require = find(exported.__requires, { name: 'Table' }); + expect(require.name).toBe('Table'); + expect(require.id).toBe('table'); + expect(require.version).toBe('1.1.1'); + }); + + it('should add heatmap panel to required', () => { + const require = find(exported.__requires, { name: 'Heatmap' }); + expect(require.name).toBe('Heatmap'); + expect(require.id).toBe('heatmap'); + expect(require.version).toBe('1.1.2'); + }); + + it('should add grafana version', () => { + const require = find(exported.__requires, { name: 'Grafana' }); + expect(require.type).toBe('grafana'); + expect(require.id).toBe('grafana'); + expect(require.version).toBe('3.0.2'); + }); + + it('should add constant template variables as inputs', () => { + const input = find(exported.__inputs, { name: 'VAR_PREFIX' }); + expect(input.type).toBe('constant'); + expect(input.label).toBe('prefix'); + expect(input.value).toBe('collectd'); + }); + + it('should templatize constant variables', () => { + const variable = find(exported.templating.list, { name: 'prefix' }); + expect(variable.query).toBe('${VAR_PREFIX}'); + expect(variable.current.text).toBe('${VAR_PREFIX}'); + expect(variable.current.value).toBe('${VAR_PREFIX}'); + expect(variable.options[0].text).toBe('${VAR_PREFIX}'); + expect(variable.options[0].value).toBe('${VAR_PREFIX}'); + }); + + it('should add datasources only use via datasource variable to requires', () => { + const require = find(exported.__requires, { name: 'OtherDB_2' }); + expect(require.id).toBe('other2'); + }); + + it('should add library panels as elements', () => { + const element: LibraryElementExport = exported.__elements['ah8NqyDPs']; + expect(element.name).toBe('Library Panel 2'); + expect(element.kind).toBe(LibraryElementKind.Panel); + expect(element.model).toEqual({ + datasource: { type: 'testdb', uid: '${DS_GFDB}' }, + type: 'graph', + }); + }); + + it('should add library panels in collapsed rows as elements', () => { + const element: LibraryElementExport = exported.__elements['jL6MrxCMz']; + expect(element.name).toBe('Library Panel'); + expect(element.kind).toBe(LibraryElementKind.Panel); + expect(element.model).toEqual({ + type: 'graph', + datasource: { + type: 'testdb', + uid: '${DS_GFDB}', + }, + }); + }); + }); +}); + +describe('dashboard exporter v2', () => { + const setup = async () => { + // Making a deep copy here because original JSON is mutated by the exporter + const schemaCopy = JSON.parse(JSON.stringify(handyTestingSchema)); + + // add a panel that uses a datasource variable + schemaCopy.elements['panel-using-datasource-var'] = { + kind: 'Panel', + spec: { + data: { + kind: 'QueryGroup', + spec: { + queries: [ + { + kind: 'PanelQuery', + spec: { + datasource: { + type: 'prometheus', + uid: '${datasourceVar}', + }, + hidden: false, + query: { + kind: 'prometheus', + spec: { + editorMode: 'builder', + expr: 'go_goroutines{job="prometheus"}', + includeNullMetadata: true, + legendFormat: '__auto', + range: true, + }, + }, + refId: 'A', + }, + }, + ], + }, + }, + }, + }; + + const dashboard = await makeExportableV2(schemaCopy); + if (typeof dashboard === 'object' && 'error' in dashboard) { + throw dashboard.error; + } + return { dashboard, originalSchema: handyTestingSchema }; + }; + + it('should replace datasource in a query variable', async () => { + const { dashboard } = await setup(); + const variable = dashboard.variables[0] as QueryVariableKind; + expect(variable.spec.datasource?.uid).toBeUndefined(); + }); + + it('do not expose datasource name and id in datasource variable', async () => { + const { dashboard } = await setup(); + const variable = dashboard.variables[2] as DatasourceVariableKind; + expect(variable.kind).toBe('DatasourceVariable'); + expect(variable.spec.current).toEqual({ text: '', value: '' }); + }); + + it('should replace datasource in annotation query', async () => { + const { dashboard } = await setup(); + const annotationQuery = dashboard.annotations[0]; + + expect(annotationQuery.spec.datasource?.uid).toBeUndefined(); + }); + + it('should remove library panels from layout', async () => { + const { dashboard, originalSchema } = await setup(); + const elementRef = 'panel-2'; + const libraryPanel = dashboard.elements[elementRef]; + const origLibraryPanel = originalSchema.elements[elementRef]; + expect(origLibraryPanel.kind).toBe('LibraryPanel'); + expect(libraryPanel).toBeUndefined(); + }); + + it('should not remove datasource ref from panel that uses a datasource variable', async () => { + const { dashboard } = await setup(); + const panel = dashboard.elements['panel-using-datasource-var']; + + if (panel.kind !== 'Panel') { + throw new Error('Panel should be a Panel'); + } + + expect(panel.spec.data.spec.queries[0].spec.datasource).toEqual({ + type: 'prometheus', + uid: '${datasourceVar}', + }); + }); +}); + +function getStubInstanceSettings(v: string | DataSourceRef): DataSourceInstanceSettings { + let key = (v as DataSourceRef)?.type ?? v; + return stubs[(key as string) ?? 'gfdb'] ?? stubs['gfdb']; +} + +// Stub responses +const stubs: { [key: string]: DataSourceInstanceSettings } = {}; +stubs['gfdb'] = { + name: 'gfdb', + meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' }, +} as DataSourceInstanceSettings; + +stubs['other'] = { + name: 'other', + meta: { id: 'other', info: { version: '1.2.1' }, name: 'OtherDB' }, +} as DataSourceInstanceSettings; + +stubs['other2'] = { + name: 'other2', + meta: { id: 'other2', info: { version: '1.2.1' }, name: 'OtherDB_2' }, +} as DataSourceInstanceSettings; + +stubs['mixed'] = { + name: 'mixed', + meta: { + id: 'mixed', + info: { version: '1.2.1' }, + name: 'Mixed', + builtIn: true, + }, +} as DataSourceInstanceSettings; + +stubs['grafana'] = { + name: '-- Grafana --', + meta: { + id: 'grafana', + info: { version: '1.2.1' }, + name: 'grafana', + builtIn: true, + }, +} as DataSourceInstanceSettings; diff --git a/public/app/features/dashboard-scene/scene/export/exporters.ts b/public/app/features/dashboard-scene/scene/export/exporters.ts new file mode 100644 index 00000000000..5c69f8a2f3b --- /dev/null +++ b/public/app/features/dashboard-scene/scene/export/exporters.ts @@ -0,0 +1,411 @@ +import { defaults, each, sortBy } from 'lodash'; + +import { DataSourceRef, PanelPluginMeta, VariableOption, VariableRefresh } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { + Spec as DashboardV2Spec, + PanelKind, + PanelQueryKind, + AnnotationQueryKind, + QueryVariableKind, + LibraryPanelRef, +} from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import config from 'app/core/config'; +import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +import { PanelModel, GridPos } from 'app/features/dashboard/state/PanelModel'; +import { getLibraryPanel } from 'app/features/library-panels/state/api'; +import { variableRegex } from 'app/features/variables/utils'; + +import { isPanelModelLibraryPanel } from '../../../library-panels/guard'; +import { LibraryElementKind } from '../../../library-panels/types'; +import { DashboardJson } from '../../../manage-dashboards/types'; +import { isConstant } from '../../../variables/guard'; + +import { removePanelRefFromLayout } from './utils'; + +export interface InputUsage { + libraryPanels?: LibraryPanelRef[]; +} + +export interface Input { + name: string; + type: string; + label: string; + value: any; + description: string; + usage?: InputUsage; +} + +interface Requires { + [key: string]: { + type: string; + id: string; + name: string; + version: string; + }; +} + +export interface ExternalDashboard { + __inputs?: Input[]; + __elements?: Record; + __requires?: Array; + panels: Array; +} + +interface PanelWithExportableLibraryPanel { + gridPos: GridPos; + id: number; + libraryPanel: LibraryPanelRef; +} + +function isExportableLibraryPanel( + p: PanelModel | PanelWithExportableLibraryPanel +): p is PanelWithExportableLibraryPanel { + return Boolean(p.libraryPanel?.name && p.libraryPanel?.uid); +} + +interface DataSources { + [key: string]: { + name: string; + label: string; + description: string; + type: string; + pluginId: string; + pluginName: string; + usage?: InputUsage; + }; +} + +export interface LibraryElementExport { + name: string; + uid: string; + model: any; + kind: LibraryElementKind; +} + +export async function makeExportableV1(dashboard: DashboardModel) { + // clean up repeated rows and panels, + // this is done on the live real dashboard instance, not on a clone + // so we need to undo this + // this is pretty hacky and needs to be changed + dashboard.cleanUpRepeats(); + + const saveModel = dashboard.getSaveModelCloneOld(); + saveModel.id = null; + + // undo repeat cleanup + dashboard.processRepeats(); + + const inputs: Input[] = []; + const requires: Requires = {}; + const datasources: DataSources = {}; + const variableLookup: { [key: string]: any } = {}; + const libraryPanels: Map = new Map(); + + for (const variable of saveModel.getVariables()) { + variableLookup[variable.name] = variable; + } + + const templateizeDatasourceUsage = (obj: any, fallback?: DataSourceRef) => { + if (obj.datasource === undefined) { + obj.datasource = fallback; + return; + } + + let datasource = obj.datasource; + let datasourceVariable: any = null; + + const datasourceUid: string | undefined = datasource?.uid; + const match = datasourceUid && variableRegex.exec(datasourceUid); + + // ignore data source properties that contain a variable + if (match) { + const varName = match[1] || match[2] || match[4]; + datasourceVariable = variableLookup[varName]; + if (datasourceVariable && datasourceVariable.current) { + datasource = datasourceVariable.current.value; + } + } + + return getDataSourceSrv() + .get(datasource) + .then((ds) => { + if (ds.meta?.builtIn) { + return; + } + + // add data source type to require list + requires['datasource' + ds.meta?.id] = { + type: 'datasource', + id: ds.meta.id, + name: ds.meta.name, + version: ds.meta.info.version || '1.0.0', + }; + + // if used via variable we can skip templatizing usage + if (datasourceVariable) { + return; + } + + const libraryPanel = obj.libraryPanel; + const libraryPanelSuffix = !!libraryPanel ? '-for-library-panel' : ''; + let refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase() + libraryPanelSuffix.toUpperCase(); + + datasources[refName] = { + name: refName, + label: ds.name, + description: '', + type: 'datasource', + pluginId: ds.meta?.id, + pluginName: ds.meta?.name, + usage: datasources[refName]?.usage, + }; + + if (!!libraryPanel) { + const libPanels = datasources[refName]?.usage?.libraryPanels || []; + libPanels.push({ name: libraryPanel.name, uid: libraryPanel.uid }); + + datasources[refName].usage = { + libraryPanels: libPanels, + }; + } + + obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' }; + }); + }; + + const processPanel = async (panel: PanelModel) => { + if (panel.type !== 'row') { + await templateizeDatasourceUsage(panel); + + if (panel.targets) { + for (const target of panel.targets) { + await templateizeDatasourceUsage(target, panel.datasource!); + } + } + + const panelDef: PanelPluginMeta = config.panels[panel.type]; + if (panelDef) { + requires['panel' + panelDef.id] = { + type: 'panel', + id: panelDef.id, + name: panelDef.name, + version: panelDef.info.version, + }; + } + } + }; + + const processLibraryPanels = async (panel: PanelModel) => { + if (isPanelModelLibraryPanel(panel)) { + const { name, uid } = panel.libraryPanel; + let model = panel.libraryPanel.model; + if (!model) { + const libPanel = await getLibraryPanel(uid, true); + model = libPanel.model; + } + + await templateizeDatasourceUsage(model); + + const { gridPos, id, ...rest } = model as any; + if (!libraryPanels.has(uid)) { + libraryPanels.set(uid, { name, uid, kind: LibraryElementKind.Panel, model: rest }); + } + } + }; + + try { + // check up panel data sources + for (const panel of saveModel.panels) { + await processPanel(panel); + + // handle collapsed rows + if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) { + for (const rowPanel of panel.panels) { + await processPanel(rowPanel); + } + } + } + + // templatize template vars + for (const variable of saveModel.getVariables()) { + if (variable.type === 'query') { + await templateizeDatasourceUsage(variable); + variable.options = []; + variable.current = {} as unknown as VariableOption; + variable.refresh = + variable.refresh !== VariableRefresh.never ? variable.refresh : VariableRefresh.onDashboardLoad; + } else if (variable.type === 'datasource') { + variable.current = {}; + } + } + + // templatize annotations vars + for (const annotationDef of saveModel.annotations.list) { + await templateizeDatasourceUsage(annotationDef); + } + + // add grafana version + requires['grafana'] = { + type: 'grafana', + id: 'grafana', + name: 'Grafana', + version: config.buildInfo.version, + }; + + // we need to process all panels again after all the promises are resolved + // so all data sources, variables and targets have been templateized when we process library panels + for (const panel of saveModel.panels) { + await processLibraryPanels(panel); + if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) { + for (const rowPanel of panel.panels) { + await processLibraryPanels(rowPanel); + } + } + } + + each(datasources, (value: any) => { + inputs.push(value); + }); + + // templatize constants + for (const variable of saveModel.getVariables()) { + if (isConstant(variable)) { + const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); + inputs.push({ + name: refName, + type: 'constant', + label: variable.label || variable.name, + value: variable.query, + description: '', + }); + // update current and option + variable.query = '${' + refName + '}'; + variable.current = { + value: variable.query, + text: variable.query, + selected: false, + }; + variable.options = [variable.current]; + } + } + + const __elements = [...libraryPanels.entries()].reduce>( + (prev, [curKey, curLibPanel]) => { + prev[curKey] = curLibPanel; + return prev; + }, + {} + ); + + // make inputs and requires a top thing + const newObj: DashboardJson = defaults( + { + __inputs: inputs, + __elements, + __requires: sortBy(requires, ['id']), + }, + saveModel + ); + + // Remove extraneous props from library panels + for (let i = 0; i < newObj.panels.length; i++) { + const libPanel = newObj.panels[i]; + if (isExportableLibraryPanel(libPanel)) { + newObj.panels[i] = { + gridPos: libPanel.gridPos, + id: libPanel.id, + libraryPanel: { uid: libPanel.libraryPanel.uid, name: libPanel.libraryPanel.name }, + }; + } + } + + return newObj; + } catch (err) { + console.error('Export failed:', err); + return { + error: err, + }; + } +} + +export async function makeExportableV2(dashboard: DashboardV2Spec) { + const variableLookup: { [key: string]: any } = {}; + + // get all datasource variables + const datasourceVariables = dashboard.variables.filter((v) => v.kind === 'DatasourceVariable'); + + for (const variable of dashboard.variables) { + variableLookup[variable.spec.name] = variable.spec; + } + + const removeDataSourceRefs = ( + obj: AnnotationQueryKind['spec'] | QueryVariableKind['spec'] | PanelQueryKind['spec'] + ) => { + const datasourceUid = obj.datasource?.uid; + if (datasourceUid?.startsWith('${') && datasourceUid?.endsWith('}')) { + const varName = datasourceUid.slice(2, -1); + // if there's a match we don't want to remove the datasource ref + const match = datasourceVariables.find((v) => v.spec.name === varName); + if (match) { + return; + } + } + + obj.datasource = undefined; + }; + + const processPanel = (panel: PanelKind) => { + if (panel.spec.data.spec.queries) { + for (const query of panel.spec.data.spec.queries) { + removeDataSourceRefs(query.spec); + } + } + }; + + try { + const elements = dashboard.elements; + const layout = dashboard.layout; + + // process elements + for (const [key, element] of Object.entries(elements)) { + if (element.kind === 'Panel') { + processPanel(element); + } else if (element.kind === 'LibraryPanel') { + // just remove the library panel + delete elements[key]; + // remove reference from layout + removePanelRefFromLayout(layout, key); + } + } + + // process template variables + for (const variable of dashboard.variables) { + if (variable.kind === 'QueryVariable') { + removeDataSourceRefs(variable.spec); + variable.spec.options = []; + variable.spec.current = { + text: '', + value: '', + }; + } else if (variable.kind === 'DatasourceVariable') { + variable.spec.current = { + text: '', + value: '', + }; + } + } + + // process annotations vars + for (const annotation of dashboard.annotations) { + removeDataSourceRefs(annotation.spec); + } + + return dashboard; + } catch (err) { + console.error('Export failed:', err); + return { + error: err, + }; + } +} diff --git a/public/app/features/dashboard-scene/scene/export/utils.ts b/public/app/features/dashboard-scene/scene/export/utils.ts new file mode 100644 index 00000000000..3cc85834662 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/export/utils.ts @@ -0,0 +1,94 @@ +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; + +/** + * Removes a panel reference from a layout. + * + * @param layout - The layout to remove the panel reference from. + * @param elementName - The name of the element to remove the panel reference from. This is the key of the element in + * the elements object. + */ +export function removePanelRefFromLayout(layout: DashboardV2Spec['layout'], elementName: string) { + switch (layout.kind) { + case 'GridLayout': { + const items = layout.spec.items || []; + layout.spec.items = items.filter((item) => { + if (item.kind === 'GridLayoutItem') { + return item.spec.element.name !== elementName; + } else if (item.kind === 'GridLayoutRow') { + item.spec.elements = item.spec.elements.filter((el) => el.spec.element.name !== elementName); + // Keep the row if it still has elements left + return item.spec.elements.length > 0; + } + return true; + }); + break; + } + + case 'AutoGridLayout': { + const items = layout.spec.items || []; + layout.spec.items = items.filter((i) => i.kind === 'AutoGridLayoutItem' && i.spec.element.name !== elementName); + break; + } + + case 'RowsLayout': { + // Each row has a nested layout, which we must process recursively + const rows = layout.spec.rows || []; + layout.spec.rows = rows.filter((row) => { + removePanelRefFromLayout(row.spec.layout, elementName); + return !isLayoutEmpty(row.spec.layout); + }); + break; + } + + case 'TabsLayout': { + // Each tab also has a nested layout, so we process it recursively + const tabs = layout.spec.tabs || []; + layout.spec.tabs = tabs.filter((tab) => { + removePanelRefFromLayout(tab.spec.layout, elementName); + return !isLayoutEmpty(tab.spec.layout); + }); + break; + } + } +} + +function isLayoutEmpty(layout: DashboardV2Spec['layout']) { + if (!layout || !layout.spec) { + return true; + } + + switch (layout.kind) { + case 'GridLayout': { + const items = layout.spec.items || []; + return ( + items.length === 0 || + items.every((item) => { + if (item.kind === 'GridLayoutItem') { + return false; + } else if (item.kind === 'GridLayoutRow') { + return item.spec.elements.length === 0; + } + return false; + }) + ); + } + + case 'AutoGridLayout': { + const items = layout.spec.items || []; + return items.length === 0; + } + + case 'RowsLayout': { + const rows = layout.spec.rows || []; + return rows.length === 0; + } + + case 'TabsLayout': { + const tabs = layout.spec.tabs || []; + return tabs.length === 0; + } + + default: + return true; + } +} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx index cbf84c9fd1d..291fa71d981 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManagerRenderer.tsx @@ -67,12 +67,20 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps -
    + {isEditing && ( +
    + + {currentTab && } + + {conditionalRenderingOverlay} +
    + )} + + {!isEditing && ( {currentTab && } - {isEditing && conditionalRenderingOverlay} -
    + )}
    ); } diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index e6f29dc9d68..373b72358c8 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -5,16 +5,20 @@ import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; +import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { getPanelPluginCounts, getV1SchemaVariables, getV2SchemaVariables, } from 'app/features/dashboard/utils/tracking'; +import { DashboardJson } from 'app/features/manage-dashboards/types'; import { DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; import { getRawDashboardChanges, getRawDashboardV2Changes } from '../saving/getDashboardChanges'; import { DashboardChangeInfo } from '../saving/shared'; import { DashboardScene } from '../scene/DashboardScene'; +import { makeExportableV1, makeExportableV2 } from '../scene/export/exporters'; +import { getVariablesCompatibility } from '../utils/getVariablesCompatibility'; import { getVizPanelKeyForPanelId } from '../utils/utils'; import { transformSceneToSaveModel } from './transformSceneToSaveModel'; @@ -25,7 +29,7 @@ import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSc * M is the type of the metadata * I is the type of the initial save model. By default it's the same as T. */ -export interface DashboardSceneSerializerLike { +export interface DashboardSceneSerializerLike { /** * The save model which the dashboard scene was originally created from */ @@ -50,6 +54,7 @@ export interface DashboardSceneSerializerLike { getElementIdForPanel: (panelId: number) => string | undefined; getElementPanelMapping: () => Map; getDSReferencesMapping: () => DSReferencesMapping; + makeExportableExternally: (s: DashboardScene) => Promise; } interface DashboardTrackingInfo { @@ -67,7 +72,9 @@ export interface DSReferencesMapping { annotations: Set; } -export class V1DashboardSerializer implements DashboardSceneSerializerLike { +export class V1DashboardSerializer + implements DashboardSceneSerializerLike +{ initialSaveModel?: Dashboard; metadata?: DashboardMeta; protected elementPanelMap = new Map(); @@ -195,6 +202,16 @@ export class V1DashboardSerializer implements DashboardSceneSerializerLike { + return getVariablesCompatibility(window.__grafanaSceneContext); + }, + }); + return await makeExportableV1(oldModel); + } } export class V2DashboardSerializer @@ -390,18 +407,36 @@ export class V2DashboardSerializer getSnapshotUrl() { return this.metadata?.annotations?.[AnnoKeyDashboardSnapshotOriginalUrl]; } + + async makeExportableExternally(s: DashboardScene) { + return await makeExportableV2(this.getSaveModel(s)); + } } -export function getDashboardSceneSerializer(): DashboardSceneSerializerLike; -export function getDashboardSceneSerializer(version: 'v1'): DashboardSceneSerializerLike; +export function getDashboardSceneSerializer(): DashboardSceneSerializerLike< + Dashboard, + DashboardMeta, + Dashboard, + DashboardJson +>; +export function getDashboardSceneSerializer( + version: 'v1' +): DashboardSceneSerializerLike; export function getDashboardSceneSerializer( version: 'v2' -): DashboardSceneSerializerLike['metadata']>; +): DashboardSceneSerializerLike< + DashboardV2Spec, + DashboardWithAccessInfo['metadata'], + DashboardV2Spec, + DashboardV2Spec +>; export function getDashboardSceneSerializer( version?: 'v1' | 'v2' ): DashboardSceneSerializerLike< Dashboard | DashboardV2Spec, - DashboardMeta | DashboardWithAccessInfo['metadata'] + DashboardMeta | DashboardWithAccessInfo['metadata'], + Dashboard | DashboardV2Spec, + DashboardJson | DashboardV2Spec > { if (version === 'v2') { return new V2DashboardSerializer(); diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 1f802147429..db3d9c0644a 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -97,7 +97,17 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model }, "description": "Test Description 2", "id": 2, - "links": [], + "links": [ + { + "targetBlank": true, + "title": "Test Link 1", + "url": "http://test1.com", + }, + { + "title": "Test Link 2", + "url": "http://test2.com", + }, + ], "title": "Test Panel 2", "vizConfig": { "kind": "graph", diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts new file mode 100644 index 00000000000..050947de00d --- /dev/null +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts @@ -0,0 +1,132 @@ +import { PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; + +import { getRuntimePanelDataSource } from './utils'; + +// Mock the config needed for the function +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + config: { + ...jest.requireActual('@grafana/runtime').config, + bootData: { + settings: { + defaultDatasource: 'default-ds-grafana', + datasources: { + 'default-ds-grafana': { + uid: 'default-ds-uid', + name: 'Default DS', + meta: { id: 'default-ds-grafana' }, + type: 'datasource', + }, + prometheus: { + uid: 'prometheus-uid', + name: 'Prometheus', + meta: { id: 'prometheus' }, + type: 'datasource', + }, + loki: { + uid: 'loki-uid', + name: 'Loki', + meta: { id: 'loki' }, + type: 'datasource', + }, + }, + }, + }, + }, +})); + +describe('getRuntimePanelDataSource', () => { + it('should return the datasource when it is specified in the query', () => { + const query: PanelQueryKind = { + kind: 'PanelQuery', + spec: { + refId: 'A', + hidden: false, + datasource: { + uid: 'test-ds-uid', + type: 'test-ds-type', + }, + query: { + kind: 'prometheus', + spec: {}, + }, + }, + }; + + const result = getRuntimePanelDataSource(query); + + expect(result).toEqual({ + uid: 'test-ds-uid', + type: 'test-ds-type', + }); + }); + + it('should infer datasource based on query kind when datasource is not specified', () => { + const query: PanelQueryKind = { + kind: 'PanelQuery', + spec: { + refId: 'A', + hidden: false, + datasource: undefined, + query: { + kind: 'prometheus', + spec: {}, + }, + }, + }; + + const result = getRuntimePanelDataSource(query); + + expect(result).toEqual({ + uid: 'prometheus-uid', + type: 'prometheus', + }); + }); + + it('should use default datasource when no datasource is specified and query kind does not match any available datasource', () => { + const query: PanelQueryKind = { + kind: 'PanelQuery', + spec: { + refId: 'A', + hidden: false, + datasource: undefined, + query: { + kind: 'unknown-type', + spec: {}, + }, + }, + }; + + const result = getRuntimePanelDataSource(query); + + expect(result).toEqual({ + uid: 'default-ds-uid', + type: 'default-ds-grafana', + }); + }); + + it('should handle the case when datasource uid is empty string', () => { + const query: PanelQueryKind = { + kind: 'PanelQuery', + spec: { + refId: 'A', + hidden: false, + datasource: { + uid: '', + type: 'test-ds-type', + }, + query: { + kind: 'prometheus', + spec: {}, + }, + }, + }; + + const result = getRuntimePanelDataSource(query); + + expect(result).toEqual({ + uid: 'prometheus-uid', + type: 'prometheus', + }); + }); +}); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index 48da1b2aa8f..c3b1133c1ec 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -186,11 +186,7 @@ function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { panel.spec.data.spec.queries.forEach((query) => { if (!datasource) { if (!query.spec.datasource?.uid) { - const defaultDatasource = config.bootData.settings.defaultDatasource; - const dsList = config.bootData.settings.datasources; - // this is look up by type - const bestGuess = Object.values(dsList).find((ds) => ds.meta.id === query.spec.query.kind); - datasource = bestGuess ? { uid: bestGuess.uid, type: bestGuess.meta.id } : dsList[defaultDatasource]; + datasource = getRuntimePanelDataSource(query); } else { datasource = query.spec.datasource; } @@ -203,26 +199,53 @@ function getPanelDataSource(panel: PanelKind): DataSourceRef | undefined { } export function getRuntimeVariableDataSource(variable: QueryVariableKind): DataSourceRef | undefined { - let datasource: DataSourceRef | undefined = undefined; + return getDataSourceForQuery(variable.spec.datasource, variable.spec.query.kind); +} - if (!datasource) { - if (!variable.spec.datasource?.uid) { - const defaultDatasource = config.bootData.settings.defaultDatasource; - const dsList = config.bootData.settings.datasources; - // this is look up by type - const bestGuess = Object.values(dsList).find((ds) => ds.meta.id === variable.spec.query.kind); - datasource = bestGuess ? { uid: bestGuess.uid, type: bestGuess.meta.id } : dsList[defaultDatasource]; - } else { - datasource = variable.spec.datasource; - } +export function getRuntimePanelDataSource(query: PanelQueryKind): DataSourceRef | undefined { + return getDataSourceForQuery(query.spec.datasource, query.spec.query.kind); +} + +/** + * @param querySpecDS - The datasource specified in the query + * @param queryKind - The kind of query being performed + * @returns The resolved DataSourceRef + */ +function getDataSourceForQuery( + querySpecDS: DataSourceRef | undefined | null, + queryKind: string +): DataSourceRef | undefined { + // If datasource is specified and has a uid, use it + if (querySpecDS?.uid) { + return querySpecDS; } - return datasource; + + // Otherwise try to infer datasource based on query kind (kind = ds type) + const defaultDatasource = config.bootData.settings.defaultDatasource; + const dsList = config.bootData.settings.datasources; + + // Look up by query type/kind + const bestGuess = dsList && Object.values(dsList).find((ds) => ds.meta.id === queryKind); + + if (bestGuess) { + return { uid: bestGuess.uid, type: bestGuess.meta.id }; + } else if (dsList && dsList[defaultDatasource]) { + // In the datasource list from bootData "id" is the type and the uid could be uid or the name + // in cases like grafana, dashboard or mixed datasource + return { + uid: dsList[defaultDatasource].uid || dsList[defaultDatasource].name, + type: dsList[defaultDatasource].meta.id, + }; + } + + // If we don't find a default datasource, return undefined + return undefined; } function panelQueryKindToSceneQuery(query: PanelQueryKind): SceneDataQuery { return { refId: query.spec.refId, - datasource: query.spec.datasource, + datasource: getRuntimePanelDataSource(query), hide: query.spec.hidden, ...query.spec.query.spec, }; diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 727964416e3..1f7f9709d67 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -72,6 +72,15 @@ jest.mock('../utils/dashboardSceneGraph', () => { // Return the panel key if it exists, otherwise use panel-1 as default return panel?.state?.key || 'panel-1'; }), + getPanelLinks: jest.fn().mockImplementation(() => { + return new VizPanelLinks({ + rawLinks: [ + { title: 'Test Link 1', url: 'http://test1.com', targetBlank: true }, + { title: 'Test Link 2', url: 'http://test2.com' }, + ], + menu: new VizPanelLinksMenu({}), + }); + }), }, }; }); diff --git a/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsJson.tsx b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsJson.tsx index dabd2bf4a28..ad37c97ca36 100644 --- a/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsJson.tsx +++ b/public/app/features/dashboard-scene/sharing/ExportButton/ExportAsJson.tsx @@ -5,7 +5,18 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { SceneComponentProps } from '@grafana/scenes'; -import { Button, ClipboardButton, CodeEditor, Label, Spinner, Stack, Switch, useStyles2 } from '@grafana/ui'; +import { + Alert, + Button, + ClipboardButton, + CodeEditor, + Label, + Spinner, + Stack, + Switch, + TextLink, + useStyles2, +} from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { createSuccessNotification } from 'app/core/copy/appNotification'; import { t, Trans } from 'app/core/internationalization'; @@ -26,21 +37,25 @@ export class ExportAsJson extends ShareExportTab { function ExportAsJsonRenderer({ model }: SceneComponentProps) { const styles = useStyles2(getStyles); - const { isSharingExternally } = model.useState(); const dashboardJson = useAsync(async () => { const json = await model.getExportableDashboardJson(); - return JSON.stringify(json, null, 2); + return json; }, [isSharingExternally]); + const stringifiedDashboardJson = JSON.stringify(dashboardJson.value?.json, null, 2); + const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels; + const isV2Dashboard = dashboardJson.value?.json && 'elements' in dashboardJson.value.json; + const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels; + const onClickDownload = async () => { await model.onSaveAsFile(); const message = t('export.json.download-successful_toast_message', 'Your JSON has been downloaded'); dispatch(notifyApp(createSuccessNotification(message))); }; - const switchLabel = t('export.json.export-externally-label', 'Export the dashboard to use in another instance'); + const switchExportLabel = t('export.json.export-externally-label', 'Export the dashboard to use in another instance'); return (
    @@ -49,24 +64,48 @@ function ExportAsJsonRenderer({ model }: SceneComponentProps) { Copy or download a JSON file containing the JSON of your dashboard

    - - - + + + + + + + {showV2LibPanelAlert && ( + + + The dynamic dashboard functionality is experimental, and has not full feature parity with current + dashboards behaviour. It is based on a new schema format, that does not support library panels. This means + that when exporting the dashboard to use it in another instance, we will not include library panels. We + intend to support them as we progress in the feature{' '} + + life cycle + + . + + + )}
    {({ width, height }) => { - if (dashboardJson.value) { + if (stringifiedDashboardJson) { return ( ) { variant="secondary" icon="copy" disabled={dashboardJson.loading} - getText={() => dashboardJson.value ?? ''} + getText={() => stringifiedDashboardJson ?? ''} onClipboardCopy={() => { DashboardInteractions.exportCopyJsonClicked(); }} diff --git a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx index 909c4e031b6..ab81fb4229c 100644 --- a/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareExportTab.tsx @@ -3,14 +3,14 @@ import { useAsync } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; -import { Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch } from '@grafana/ui'; +import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Alert, Button, ClipboardButton, CodeEditor, Field, Modal, Stack, Switch, TextLink } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; -import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal'; +import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; -import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; +import { DashboardJson } from 'app/features/manage-dashboards/types'; -import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; -import { getVariablesCompatibility } from '../utils/getVariablesCompatibility'; import { DashboardInteractions } from '../utils/interactions'; import { getDashboardSceneFor } from '../utils/utils'; @@ -25,8 +25,6 @@ export class ShareExportTab extends SceneObjectBase impleme public tabId = shareDashboardType.export; static Component = ShareExportTabRenderer; - private _exporter = new DashboardExporter(); - constructor(state: Omit) { super({ isSharingExternally: false, @@ -55,26 +53,33 @@ export class ShareExportTab extends SceneObjectBase impleme return; } - public getExportableDashboardJson = async () => { + public getExportableDashboardJson = async (): Promise<{ + json: Dashboard | DashboardJson | DashboardV2Spec | { error: unknown }; + hasLibraryPanels?: boolean; + }> => { const { isSharingExternally } = this.state; - const saveModel = transformSceneToSaveModel(getDashboardSceneFor(this)); - const exportable = isSharingExternally - ? await this._exporter.makeExportable( - new DashboardModel(saveModel, undefined, { - getVariablesFromState: () => { - return getVariablesCompatibility(window.__grafanaSceneContext); - }, - }) - ) - : saveModel; + const scene = getDashboardSceneFor(this); + const exportableDashboard = await scene.serializer.makeExportableExternally(scene); + const origDashboard = scene.serializer.getSaveModel(scene); + const exportable = isSharingExternally ? exportableDashboard : origDashboard; - return exportable; + if (isDashboardV2Spec(origDashboard)) { + return { + json: exportable, + hasLibraryPanels: Object.values(origDashboard.elements).some((element) => element.kind === 'LibraryPanel'), + }; + } + + return { + json: exportable, + hasLibraryPanels: undefined, + }; }; public onSaveAsFile = async () => { - const dashboardJson = await this.getExportableDashboardJson(); - const dashboardJsonPretty = JSON.stringify(dashboardJson, null, 2); + const dashboard = await this.getExportableDashboardJson(); + const dashboardJsonPretty = JSON.stringify(dashboard.json, null, 2); const { isSharingExternally } = this.state; const blob = new Blob([dashboardJsonPretty], { @@ -83,8 +88,8 @@ export class ShareExportTab extends SceneObjectBase impleme const time = new Date().getTime(); let title = 'dashboard'; - if ('title' in dashboardJson && dashboardJson.title) { - title = dashboardJson.title; + if ('title' in dashboard.json && dashboard.json.title) { + title = dashboard.json.title; } saveAs(blob, `${title}-${time}.json`); DashboardInteractions.exportDownloadJsonClicked({ @@ -95,14 +100,17 @@ export class ShareExportTab extends SceneObjectBase impleme function ShareExportTabRenderer({ model }: SceneComponentProps) { const { isSharingExternally, isViewingJSON, modalRef } = model.useState(); - const dashboardJson = useAsync(async () => { - if (isViewingJSON) { - const json = await model.getExportableDashboardJson(); - return JSON.stringify(json, null, 2); - } - return ''; - }, [isViewingJSON]); + const dashboardJson = useAsync(async () => { + const json = await model.getExportableDashboardJson(); + return json; + }, [isViewingJSON, isSharingExternally]); + + const stringifiedDashboardJson = JSON.stringify(dashboardJson.value?.json, null, 2); + const hasLibraryPanels = dashboardJson.value?.hasLibraryPanels; + + const isV2Dashboard = dashboardJson.value?.json && 'elements' in dashboardJson.value.json; + const showV2LibPanelAlert = isV2Dashboard && isSharingExternally && hasLibraryPanels; const exportExternallyTranslation = t('share-modal.export.share-externally-label', `Export for sharing externally`); @@ -121,6 +129,26 @@ function ShareExportTabRenderer({ model }: SceneComponentProps) onChange={model.onShareExternallyChange} /> + {showV2LibPanelAlert && ( + + + The dynamic dashboard functionality is experimental, and has not full feature parity with current + dashboards behaviour. It is based on a new schema format, that does not support library panels. This + means that when exporting the dashboard to use it in another instance, we will not include library + panels. We intend to support them as we progress in the feature{' '} + + life cycle + + . + + + )} @@ -149,7 +177,8 @@ function ShareExportTabRenderer({ model }: SceneComponentProps) if (dashboardJson.value) { return ( ) variant="secondary" icon="copy" disabled={dashboardJson.loading} - getText={() => dashboardJson.value ?? ''} + getText={() => stringifiedDashboardJson ?? ''} > Copy to Clipboard diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx new file mode 100644 index 00000000000..4df6d8faf52 --- /dev/null +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardFormV2.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from 'react'; +import { Controller, FieldErrors, UseFormReturn } from 'react-hook-form'; + +import { selectors } from '@grafana/e2e-selectors'; +import { ExpressionDatasourceRef } from '@grafana/runtime/internal'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { Button, Field, FormFieldErrors, FormsOnSubmit, Stack, Input } from '@grafana/ui'; +import { FolderPicker } from 'app/core/components/Select/FolderPicker'; +import { t, Trans } from 'app/core/internationalization'; +import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; +import { DashboardInputs, DataSourceInput } from 'app/features/manage-dashboards/state/reducers'; +import { validateTitle } from 'app/features/manage-dashboards/utils/validation'; +interface Props + extends Pick< + UseFormReturn & { [key: `datasource-${string}`]: string }>, + 'register' | 'control' | 'getValues' | 'watch' + > { + inputs: DashboardInputs; + uidReset: boolean; + errors: FieldErrors & { [key: `datasource-${string}`]: string }>; + onCancel: () => void; + onUidReset: () => void; + onSubmit: FormsOnSubmit & { [key: `datasource-${string}`]: string }>; +} + +export const ImportDashboardFormV2 = ({ + register, + errors, + control, + inputs, + getValues, + uidReset, + onUidReset, + onCancel, + onSubmit, + watch, +}: Props) => { + const [isSubmitted, setSubmitted] = useState(false); + const [selectedDataSources, setSelectedDataSources] = useState>({}); + /* + This useEffect is needed for overwriting a dashboard. It + submits the form even if there's validation errors on title or uid. + */ + useEffect(() => { + if (isSubmitted && (errors.dashboard?.title || errors.k8s?.name)) { + const formValues = getValues(); + onSubmit({ + ...formValues, + dashboard: { + ...formValues.dashboard, + title: formValues.dashboard.title, + }, + }); + } + }, [errors, getValues, isSubmitted, onSubmit]); + + return ( + <> + Options + + await validateTitle(v, getValues().folderUid ?? ''), + })} + type="text" + data-testid={selectors.components.ImportDashboardForm.name} + /> + + + + render={({ field: { ref, value, onChange, ...field } }) => ( + { + onChange(uid, title); + }} + value={value} + /> + )} + name="folderUid" + control={control} + /> + + {inputs.dataSources && + inputs.dataSources.map((input: DataSourceInput) => { + if (input.pluginId === ExpressionDatasourceRef.type) { + return null; + } + + const dataSourceOption = `datasource-${input.pluginId}` as const; + + return ( + + + name={dataSourceOption} + render={({ field: { ref, ...field } }) => ( + { + field.onChange(ds); + // Update our selected datasources map + setSelectedDataSources((prev) => ({ + ...prev, + [input.pluginId]: { + uid: ds.uid, + type: ds.type, + }, + })); + }} + /> + )} + control={control} + rules={{ required: true }} + /> + + ); + })} + + + + + + + ); +}; + +function getButtonVariant( + errors: FormFieldErrors & { [key: `datasource-${string}`]: string }> +) { + return errors && (errors.dashboard?.title || errors.k8s?.name) ? 'destructive' : 'primary'; +} + +function getButtonText( + errors: FormFieldErrors & { [key: `datasource-${string}`]: string }> +) { + return errors && (errors.dashboard?.title || errors.k8s?.name) ? 'Import (Overwrite)' : 'Import'; +} diff --git a/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx new file mode 100644 index 00000000000..ef0262adec5 --- /dev/null +++ b/public/app/features/dashboard-scene/v2schema/ImportDashboardOverviewV2.tsx @@ -0,0 +1,181 @@ +import { useState } from 'react'; + +import { locationUtil } from '@grafana/data'; +import { locationService, reportInteraction } from '@grafana/runtime'; +import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0'; +import { AnnotationQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { Form } from 'app/core/components/Form/Form'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; +import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; +import { clearLoadedDashboard } from 'app/features/manage-dashboards/state/actions'; +import { useDispatch, useSelector, StoreState } from 'app/types'; + +import { ImportDashboardFormV2 } from './ImportDashboardFormV2'; + +const IMPORT_FINISHED_EVENT_NAME = 'dashboard_import_imported'; + +export function ImportDashboardOverviewV2() { + const [uidReset, setUidReset] = useState(false); + const dispatch = useDispatch(); + + // Get state from Redux store + const searchObj = locationService.getSearchObject(); + const dashboard = useSelector((state: StoreState) => state.importDashboard.dashboard as DashboardV2Spec); + const inputs = useSelector((state: StoreState) => state.importDashboard.inputs); + const folder = searchObj.folderUid ? { uid: String(searchObj.folderUid) } : { uid: '' }; + + function onUidReset() { + setUidReset(true); + } + + function onCancel() { + dispatch(clearLoadedDashboard()); + } + + async function onSubmit(form: SaveDashboardCommand) { + reportInteraction(IMPORT_FINISHED_EVENT_NAME); + + const dashboardWithDataSources: DashboardV2Spec = { + ...dashboard, + title: form.dashboard.title, + annotations: dashboard.annotations?.map((annotation: AnnotationQueryKind) => { + if (annotation.spec.datasource?.type) { + const dsType = annotation.spec.datasource.type; + if (form[`datasource-${dsType}` as keyof typeof form]) { + const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string }; + return { + ...annotation, + spec: { + ...annotation.spec, + datasource: { + uid: ds.uid, + type: ds.type, + }, + }, + }; + } + } + return annotation; + }), + variables: dashboard.variables?.map((variable) => { + if (variable.kind === 'QueryVariable') { + if (variable.spec.datasource?.type) { + const dsType = variable.spec.datasource.type; + if (form[`datasource-${dsType}` as keyof typeof form]) { + const ds = form[`datasource-${dsType}` as keyof typeof form] as { uid: string; type: string }; + return { + ...variable, + spec: { + ...variable.spec, + datasource: { + ...variable.spec.datasource, + uid: ds.uid, + type: ds.type, + }, + options: [], + current: { + text: '', + value: '', + }, + refresh: 'onDashboardLoad', + }, + }; + } + } + } else if (variable.kind === 'DatasourceVariable') { + return { + ...variable, + spec: { + ...variable.spec, + current: { + text: '', + value: '', + }, + }, + }; + } + return variable; + }), + elements: Object.fromEntries( + Object.entries(dashboard.elements).map(([key, element]) => { + if (element.kind === 'Panel') { + const panel = { ...element.spec }; + if (panel.data?.kind === 'QueryGroup') { + const newQueries = panel.data.spec.queries.map((query: any) => { + if (query.kind === 'PanelQuery') { + const queryType = query.spec.query?.kind; + // Match datasource by query kind + if (queryType && form[`datasource-${queryType}` as keyof typeof form]) { + const ds = form[`datasource-${queryType}` as keyof typeof form] as { uid: string; type: string }; + return { + ...query, + spec: { + ...query.spec, + datasource: { + uid: ds.uid, + type: ds.type, + }, + }, + }; + } + } + return query; + }); + panel.data = { + ...panel.data, + spec: { + ...panel.data.spec, + queries: newQueries, + }, + }; + } + return [ + key, + { + kind: element.kind, + spec: panel, + }, + ]; + } + return [key, element]; + }) + ), + }; + + const result = await getDashboardAPI('v2').saveDashboard({ + ...form, + dashboard: dashboardWithDataSources, + }); + + if (result.url) { + const dashboardUrl = locationUtil.stripBaseFromUrl(result.url); + locationService.push(dashboardUrl); + } + } + + return ( + <> + & { [key: `datasource-${string}`]: string }> + onSubmit={onSubmit} + defaultValues={{ dashboard, k8s: { annotations: { 'grafana.app/folder': folder.uid } } }} + validateOnMount + validateOn="onChange" + > + {({ register, errors, control, watch, getValues }) => ( + + )} + + + ); +} diff --git a/public/app/features/dashboard/components/ShareModal/ShareExport.tsx b/public/app/features/dashboard/components/ShareModal/ShareExport.tsx index 46341ce195f..74255c8b869 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareExport.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareExport.tsx @@ -5,6 +5,7 @@ import { Button, Field, Modal, Switch } from '@grafana/ui'; import { appEvents } from 'app/core/core'; import { t, Trans } from 'app/core/internationalization'; import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal'; +import { makeExportableV1 } from 'app/features/dashboard-scene/scene/export/exporters'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; import { ShowModalReactEvent } from 'app/types/events'; @@ -46,7 +47,7 @@ export class ShareExport extends PureComponent { }); if (shareExternally) { - this.exporter.makeExportable(dashboard).then((dashboardJson) => { + makeExportableV1(dashboard).then((dashboardJson) => { this.openSaveAsDialog(dashboardJson); }); } else { diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index d788365f4d5..c3629145e15 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -13,7 +13,6 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { DashboardDTO } from 'app/types'; import { appEvents } from '../../../core/core'; -import { loadDashboardFromProvisioning } from '../../provisioning/dashboardLoader'; import { ResponseTransformers } from '../api/ResponseTransformers'; import { getDashboardAPI } from '../api/dashboard_api'; import { DashboardVersionError, DashboardWithAccessInfo } from '../api/types'; @@ -37,6 +36,7 @@ abstract class DashboardLoaderSrvBase implements DashboardLoaderSrvLike { uid: string | undefined, params?: UrlQueryMap ): Promise; + abstract loadSnapshot(slug: string): Promise; protected loadScriptedDashboard(file: string) { @@ -124,10 +124,6 @@ export class DashboardLoaderSrv extends DashboardLoaderSrvBase { if (type === 'script' && slug) { promise = this.loadScriptedDashboard(slug); - } else if (type === 'provisioning' && uid && slug) { - promise = loadDashboardFromProvisioning(slug, uid); - // needed for the old architecture - // in scenes this is handled through loadSnapshot method } else if (type === 'snapshot' && slug) { promise = getDashboardSnapshotSrv().getSnapshot(slug); } else if (type === 'public' && uid) { @@ -200,8 +196,6 @@ export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase { return ResponseTransformers.ensureV2Response(result); }); - } else if (type === 'provisioning' && uid && slug) { - promise = loadDashboardFromProvisioning(slug, uid).then((r) => ResponseTransformers.ensureV2Response(r)); } else if (uid) { if (!params) { const cachedDashboard = stateManager.getDashboardFromCache(uid); @@ -263,5 +257,6 @@ export const setDashboardLoaderSrv = (srv: DashboardLoaderSrv) => { if (process.env.NODE_ENV !== 'test') { throw new Error('dashboardLoaderSrv can be only overriden in test environment'); } + dashboardLoaderSrv = srv; }; diff --git a/public/app/features/expressions/utils/expressionTypes.ts b/public/app/features/expressions/utils/expressionTypes.ts index dd648512b85..b4487f6c350 100644 --- a/public/app/features/expressions/utils/expressionTypes.ts +++ b/public/app/features/expressions/utils/expressionTypes.ts @@ -86,3 +86,15 @@ export function isReducerExpression(expressionModel: ExpressionQuery) { export function isThresholdExpression(expressionModel: ExpressionQuery) { return expressionModel.type === ExpressionQueryType.threshold; } + +/** + * Determines if the given evaluator function type is a range type (requiring two threshold values) + */ +export function isRangeEvaluator(evalFunction: EvalFunction): boolean { + return ( + evalFunction === EvalFunction.IsWithinRange || + evalFunction === EvalFunction.IsOutsideRange || + evalFunction === EvalFunction.IsOutsideRangeIncluded || + evalFunction === EvalFunction.IsWithinRangeIncluded + ); +} diff --git a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx index b1eb1d196b0..a03dcbb1d5f 100644 --- a/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/AddLibraryPanelModal/AddLibraryPanelModal.tsx @@ -60,7 +60,6 @@ export const AddLibraryPanelContents = ({ } }, [debouncedPanelName, folderUid]); - console.log('isValidName:', isValidName); const invalidInput = !isValidName?.value && isValidName.value !== undefined && panelName === debouncedPanelName && !waiting; diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index bfebb0cec81..43297d1ec37 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -27,12 +27,14 @@ import { Form } from 'app/core/components/Form/Form'; import { Page } from 'app/core/components/Page/Page'; import { t, Trans } from 'app/core/internationalization'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; +import { dispatch } from 'app/store/store'; import { StoreState } from 'app/types'; import { cleanUpAction } from '../../core/actions/cleanUp'; +import { ImportDashboardOverviewV2 } from '../dashboard-scene/v2schema/ImportDashboardOverviewV2'; import { ImportDashboardOverview } from './components/ImportDashboardOverview'; -import { fetchGcomDashboard, importDashboardJson } from './state/actions'; +import { fetchGcomDashboard, importDashboardJson, importDashboardV2Json } from './state/actions'; import { initialImportDashboardState } from './state/reducers'; import { validateDashboardJson, validateGcomDashboard } from './utils/validation'; @@ -53,6 +55,7 @@ const JSON_PLACEHOLDER = `{ const mapStateToProps = (state: StoreState) => ({ loadingState: state.importDashboard.state, + dashboard: state.importDashboard.dashboard, }); const mapDispatchToProps = { @@ -88,7 +91,13 @@ class UnthemedDashboardImport extends PureComponent { }); try { - this.props.importDashboardJson(JSON.parse(String(result))); + const json = JSON.parse(String(result)); + + if (json.elements) { + dispatch(importDashboardV2Json(json)); + return; + } + this.props.importDashboardJson(json); } catch (error) { if (error instanceof Error) { appEvents.emit(AppEvents.alertError, ['Import failed', 'JSON -> JS Serialization failed: ' + error.message]); @@ -102,7 +111,14 @@ class UnthemedDashboardImport extends PureComponent { import_source: 'json_pasted', }); - this.props.importDashboardJson(JSON.parse(formData.dashboardJson)); + const dashboard = JSON.parse(formData.dashboardJson); + + if (dashboard.elements) { + dispatch(importDashboardV2Json(dashboard)); + return; + } + + this.props.importDashboardJson(dashboard); }; getGcomDashboard = (formData: { gcomDashboard: string }) => { @@ -229,6 +245,19 @@ class UnthemedDashboardImport extends PureComponent { subTitle: 'Import dashboard from file or Grafana.com', }; + getDashboardOverview() { + const { loadingState, dashboard } = this.props; + + if (loadingState === LoadingState.Done) { + if (dashboard.elements) { + return ; + } + return ; + } + + return null; + } + render() { const { loadingState } = this.props; @@ -243,7 +272,7 @@ class UnthemedDashboardImport extends PureComponent { )} {[LoadingState.Error, LoadingState.NotStarted].includes(loadingState) && this.renderImportForm()} - {loadingState === LoadingState.Done && } + {this.getDashboardOverview()} ); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 48a8462a7f9..a5ddf74bbc6 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,8 +1,10 @@ import { DataSourceInstanceSettings } from '@grafana/data'; import { getBackendSrv, getDataSourceSrv, isFetchError } from '@grafana/runtime'; +import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { browseDashboardsAPI, ImportInputs } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; import { @@ -18,6 +20,7 @@ import { DashboardJson } from '../types'; import { clearDashboard, + DataSourceInput, fetchDashboard, fetchFailed, ImportDashboardDTO, @@ -56,6 +59,13 @@ export function importDashboardJson(dashboard: any): ThunkResult { }; } +export function importDashboardV2Json(dashboard: DashboardV2Spec): ThunkResult { + return async (dispatch) => { + dispatch(setJsonDashboard(dashboard)); + dispatch(processV2Elements(dashboard)); + }; +} + const getNewLibraryPanelsByInput = (input: Input, state: ImportDashboardState): LibraryPanel[] | undefined => { return input?.usage?.libraryPanels?.filter((usageLibPanel) => state.inputs.libraryPanels.some( @@ -142,6 +152,53 @@ function processElements(dashboardJson?: { __elements?: Record { + return async function (dispatch) { + const elements = dashboard.elements; + // get elements from dashboard + // each element can only be a panel + const inputs: Record = {}; + for (const element of Object.values(elements)) { + if (element.kind !== 'Panel') { + throw new Error('Only panels are currenlty supported in v2 dashboards'); + } + + for (const query of element.spec.data.spec.queries) { + const datasourceRef = query.spec.datasource; + if (!datasourceRef) { + let dataSourceInput: DataSourceInput | undefined; + const dsType = query.spec.query.kind; + const datasource = await getDatasourceSrv().get({ type: dsType }); + if (!datasource) { + dataSourceInput = { + name: dsType, + label: dsType, + info: `No data sources of type ${dsType} found`, + value: '', + type: InputType.DataSource, + pluginId: dsType, + }; + + inputs[dsType] = dataSourceInput; + } else { + dataSourceInput = { + name: datasource.name, + label: datasource.name, + info: `Select a ${datasource.name} data source`, + value: datasource.uid, + type: InputType.DataSource, + pluginId: datasource.meta?.id, + }; + + inputs[datasource.meta?.id] = dataSourceInput; + } + } + } + } + dispatch(setInputs(Object.values(inputs))); + }; +} + export async function getLibraryPanelInputs(dashboardJson?: { __elements?: Record; }): Promise { diff --git a/public/app/features/plugins/admin/pages/Browse.tsx b/public/app/features/plugins/admin/pages/Browse.tsx index 38b9447cf0c..f28fbf04331 100644 --- a/public/app/features/plugins/admin/pages/Browse.tsx +++ b/public/app/features/plugins/admin/pages/Browse.tsx @@ -8,6 +8,7 @@ import { Select, RadioButtonGroup, useStyles2, Tooltip, Field, TextLink } from ' import { Page } from 'app/core/components/Page/Page'; import { t, Trans } from 'app/core/internationalization'; import { getNavModel } from 'app/core/selectors/navModel'; +import { AdvisorRedirectNotice } from 'app/features/connections/components/AdvisorRedirectNotice/AdvisorRedirectNotice'; import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { useSelector } from 'app/types'; @@ -94,6 +95,7 @@ export default function Browse() { return ( + diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 4ba153fe94d..dbf88a26156 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -20,7 +20,7 @@ import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime'; import { Modal } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; -import { ShowModalReactEvent } from 'app/types/events'; +import { OpenExtensionSidebarEvent, ShowModalReactEvent } from 'app/types/events'; import { ExtensionsLog, log } from './logs/log'; import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry'; @@ -375,6 +375,15 @@ export function getLinkExtensionOnClick( const helpers: PluginExtensionEventHelpers = { context, openModal: createOpenModalFunction(pluginId), + openSidebar: (componentTitle, context) => { + appEvents.publish( + new OpenExtensionSidebarEvent({ + props: context, + pluginId, + componentTitle, + }) + ); + }, }; log.debug(`onClick '${config.title}' at '${extensionPointId}'`); diff --git a/public/app/features/plugins/loader/packageMetrics.test.ts b/public/app/features/plugins/loader/packageMetrics.test.ts deleted file mode 100644 index eb06454acaf..00000000000 --- a/public/app/features/plugins/loader/packageMetrics.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { logInfo } from '@grafana/runtime'; - -import { trackPackageUsage } from './packageMetrics'; - -jest.mock('@grafana/runtime', () => ({ - logInfo: jest.fn().mockImplementation(), -})); - -// notice each test object has a different key to prevent hitting the cache -const logInfoMock = logInfo as jest.Mock; -const mockUsage = jest.fn(); - -describe('trackPackageUsage', () => { - beforeEach(() => { - logInfoMock.mockClear(); - }); - - describe('With document.currentScript null', () => { - const originalCurrentScript = document.currentScript; - - // set currentScript to null - beforeAll(() => { - Object.defineProperty(document, 'currentScript', { - value: null, - writable: true, - }); - }); - - // restore original currentScript - afterAll(() => { - Object.defineProperty(document, 'currentScript', { - value: originalCurrentScript, - writable: true, - }); - }); - - it('should not log API usage and for non-grafana packages', () => { - const obj = { - foo: 'bar', - }; - const packageName = 'lodash'; - - const result = trackPackageUsage(obj, packageName); - - mockUsage(result.foo); - - expect(logInfoMock).toHaveBeenCalledTimes(0); - }); - - it('should log API usage and return a proxy object for @grafana/data packages', () => { - const obj = { - foo: 'bar', - }; - const packageName = '@grafana/data'; - - const result = trackPackageUsage(obj, packageName); - - mockUsage(result.foo); - - expect(logInfoMock).toHaveBeenCalledTimes(1); - expect(logInfoMock).toHaveBeenLastCalledWith(`Plugin using @grafana/data.foo`, { - key: 'foo', - parent: '@grafana/data', - packageName: '@grafana/data', - }); - expect(result).toEqual(obj); - }); - - it('should log API usage and return a proxy object for app/* packages', () => { - const obj = { - foo: 'bar', - }; - const packageName = 'app/core/test'; - - const result = trackPackageUsage(obj, packageName); - - mockUsage(result.foo); - - expect(logInfoMock).toHaveBeenCalledTimes(1); - expect(logInfoMock).toHaveBeenLastCalledWith(`Plugin using app/core/test.foo`, { - key: 'foo', - parent: 'app/core/test', - packageName: 'app/core/test', - }); - expect(result).toEqual(obj); - }); - - it('should return a proxy object for nested properties', () => { - const obj = { - foo2: { - bar: 'baz', - }, - }; - const packageName = '@grafana/data'; - - const result = trackPackageUsage(obj, packageName); - mockUsage(result.foo2.bar); - - // 2 calls, one for each attribute - expect(logInfoMock).toHaveBeenCalledTimes(2); - - expect(logInfoMock).toHaveBeenCalledWith(`Plugin using @grafana/data.foo2`, { - key: 'foo2', - parent: '@grafana/data', - packageName: '@grafana/data', - }); - expect(logInfoMock).toHaveBeenCalledWith(`Plugin using @grafana/data.foo2.bar`, { - key: 'bar', - parent: '@grafana/data.foo2', - packageName: '@grafana/data', - }); - - expect(result.foo2).toEqual(obj.foo2); - }); - - it('should not log API usage for symbols or __useDefault key', () => { - const obj = { - [Symbol('key')]: 'value', - __useDefault: 'default', - }; - const packageName = '@grafana/data'; - - const result = trackPackageUsage(obj, packageName); - - expect(logInfoMock).not.toHaveBeenCalled(); - expect(result).toEqual(obj); - }); - - it('should return the same proxy object for the same nested property', () => { - const obj = { - foo3: { - bar: 'baz', - }, - }; - const packageName = '@grafana/data'; - - const result1 = trackPackageUsage(obj, packageName); - const result2 = trackPackageUsage(obj, packageName); - - mockUsage(result1.foo3); - - expect(logInfoMock).toHaveBeenCalledTimes(1); - expect(logInfoMock).toHaveBeenCalledWith(`Plugin using @grafana/data.foo3`, { - key: 'foo3', - parent: '@grafana/data', - packageName: '@grafana/data', - }); - mockUsage(result2.foo3.bar); - expect(logInfoMock).toHaveBeenCalledWith(`Plugin using @grafana/data.foo3.bar`, { - key: 'bar', - parent: '@grafana/data.foo3', - packageName: '@grafana/data', - }); - - expect(result1.foo3).toEqual(obj.foo3); - expect(result2.foo3).toEqual(obj.foo3); - expect(result1.foo3).toBe(result2.foo3); - }); - - it('should not report twice the same key usage', () => { - const obj = { - cacheMe: 'please', - zap: { - cacheMeInner: 'please', - }, - }; - - const result = trackPackageUsage(obj, '@grafana/data'); - - mockUsage(result.cacheMe); - expect(logInfoMock).toHaveBeenCalledTimes(1); - mockUsage(result.cacheMe); - expect(logInfoMock).toHaveBeenCalledTimes(1); - - mockUsage(result.zap); - expect(logInfoMock).toHaveBeenCalledTimes(2); - mockUsage(result.zap); - expect(logInfoMock).toHaveBeenCalledTimes(2); - - mockUsage(result.zap.cacheMeInner); - expect(logInfoMock).toHaveBeenCalledTimes(3); - mockUsage(result.zap.cacheMeInner); - expect(logInfoMock).toHaveBeenCalledTimes(3); - - expect(result).toEqual(obj); - }); - }); - - it('Should skip tracking if document.currentScript is not null', () => { - // Save the original value of the attribute - const originalCurrentScript = document.currentScript; - - // Define a new property on the document object with the mock currentScript - Object.defineProperty(document, 'currentScript', { - value: { - src: 'mocked-script.js', - }, - writable: true, - }); - - const obj = { - lor: 'me', - }; - - const result = trackPackageUsage(obj, '@grafana/data'); - - mockUsage(result.lor); - expect(logInfoMock).not.toHaveBeenCalled(); - - // Restore the original value of the currentScript attribute - Object.defineProperty(document, 'currentScript', { - value: originalCurrentScript, - writable: true, - }); - }); -}); diff --git a/public/app/features/plugins/loader/packageMetrics.ts b/public/app/features/plugins/loader/packageMetrics.ts deleted file mode 100644 index a8ef34e1416..00000000000 --- a/public/app/features/plugins/loader/packageMetrics.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { logInfo } from '@grafana/runtime'; - -const cachedMetricProxies = new WeakMap(); -const trackedKeys: Record = {}; - -function createMetricsProxy(obj: T, parentName: string, packageName: string): T { - const handler: ProxyHandler = { - get(target, key) { - if ( - // plugins are evaluated by SystemJS and not by a browser