From f0270d8e312006bdf38387c7909742e91a1262ed Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:05:55 +0200 Subject: [PATCH 001/122] Update code owners for O11Y package (#89334) --- .github/CODEOWNERS | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6136bd07605..f5da47a35b3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -313,37 +313,47 @@ /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources /e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend + +# Packages /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-data/src/**/*logs* @grafana/observability-logs +/packages/grafana-data/src/transformations/ @grafana/dataviz-squad /packages/grafana-e2e-selectors/ @grafana/grafana-frontend-platform +/packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/ @grafana/observability-logs +/packages/grafana-o11y-ds-frontend/src/IntervalInput/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/NodeGraph/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/pyroscope/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/SpanBar/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/TraceToLogs/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/TraceToMetrics/ @grafana/observability-traces-and-profiling +/packages/grafana-o11y-ds-frontend/src/TraceToProfiles/ @grafana/observability-traces-and-profiling +/packages/grafana-plugin-configs/ @grafana/plugins-platform-frontend +/packages/grafana-prometheus/ @grafana/observability-metrics +/packages/grafana-schema/src/**/*canvas* @grafana/dataviz-squad +/packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling +/packages/grafana-sql/ @grafana/partner-datasources @grafana/oss-big-tent /packages/grafana-ui/.storybook/ @grafana/plugins-platform-frontend /packages/grafana-ui/src/components/ @grafana/grafana-frontend-platform +/packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad +/packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DateTimePickers/ @grafana/grafana-frontend-platform +/packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad +/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations /packages/grafana-ui/src/components/Table/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Table/SparklineCell.tsx @grafana/dataviz-squad @grafana/app-o11y-visualizations -/packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/uPlot/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/ValuePicker/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizLayout/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizLegend/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations /packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend -/packages/grafana-data/src/transformations/ @grafana/dataviz-squad -/packages/grafana-data/src/**/*logs* @grafana/observability-logs -/packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling -/packages/grafana-schema/src/**/*canvas* @grafana/dataviz-squad -/packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling + /plugins-bundled/ @grafana/plugins-platform-frontend -/packages/grafana-plugin-configs/ @grafana/plugins-platform-frontend -/packages/grafana-prometheus/ @grafana/observability-metrics -/packages/grafana-o11y-ds-frontend/ @grafana/observability-logs @grafana/observability-traces-and-profiling -/packages/grafana-sql/ @grafana/partner-datasources @grafana/oss-big-tent # root files, mostly frontend /.browserslistrc @grafana/frontend-ops From 6597bed9f78581ec8dab59c6d8ca9f907f35c71b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 18 Jun 2024 17:26:02 +0100 Subject: [PATCH 002/122] Chore: Bump ws (#89371) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index be5bd5342fc..4529834161a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30966,8 +30966,8 @@ __metadata: linkType: hard "ws@npm:^8.16.0, ws@npm:^8.2.3, ws@npm:^8.9.0": - version: 8.16.0 - resolution: "ws@npm:8.16.0" + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -30976,7 +30976,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/7c511c59e979bd37b63c3aea4a8e4d4163204f00bd5633c053b05ed67835481995f61a523b0ad2b603566f9a89b34cb4965cb9fab9649fbfebd8f740cea57f17 + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d languageName: node linkType: hard From 966cee864a227cc5d2a060e1d2797b317328165a Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 18 Jun 2024 18:34:39 +0200 Subject: [PATCH 003/122] LogRows: add missing call to close the popover (#89370) --- public/app/features/logs/components/LogRows.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 87306b91e2a..20556b9f232 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -147,6 +147,7 @@ class UnThemedLogRows extends PureComponent { if (document.getSelection()?.toString()) { return; } + this.closePopoverMenu(); }; closePopoverMenu = () => { From 59f255bb7fbe48818b4e0346e321b66ddb39bf6c Mon Sep 17 00:00:00 2001 From: Travis Patterson Date: Tue, 18 Jun 2024 10:54:14 -0600 Subject: [PATCH 004/122] Add info tooltips to logs that have been sampled by adaptive logs (#89320) * Add info tooltips to logs that have been sampled by adaptive logs * review commends and linter changes * make warnings match info * fix betterer results --- .betterer.results | 3 +-- public/app/features/logs/components/LogRow.tsx | 14 ++++++++++++-- .../features/logs/components/getLogRowStyles.ts | 13 ++++++++++--- public/app/features/logs/utils.ts | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/.betterer.results b/.betterer.results index d266b08b826..87b8a31de53 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4702,8 +4702,7 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "31"], [0, 0, 0, "Styles should be written using objects.", "32"], [0, 0, 0, "Styles should be written using objects.", "33"], - [0, 0, 0, "Styles should be written using objects.", "34"], - [0, 0, 0, "Styles should be written using objects.", "35"] + [0, 0, 0, "Styles should be written using objects.", "34"] ], "public/app/features/logs/components/log-context/LogContextButtons.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] diff --git a/public/app/features/logs/components/LogRow.tsx b/public/app/features/logs/components/LogRow.tsx index fe161e3111e..a06edb4bdf1 100644 --- a/public/app/features/logs/components/LogRow.tsx +++ b/public/app/features/logs/components/LogRow.tsx @@ -17,7 +17,7 @@ import { reportInteraction } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { withTheme2, Themeable2, Icon, Tooltip, PopoverContent } from '@grafana/ui'; -import { checkLogsError, escapeUnescapedString } from '../utils'; +import { checkLogsError, escapeUnescapedString, checkLogsSampled } from '../utils'; import { LogDetails } from './LogDetails'; import { LogLabels } from './LogLabels'; @@ -222,6 +222,7 @@ class UnThemedLogRow extends PureComponent { const { showDetails, showingContext, permalinked } = this.state; const levelStyles = getLogLevelStyles(theme, row.logLevel); const { errorMessage, hasError } = checkLogsError(row); + const { sampleMessage, isSampled } = checkLogsSampled(row); const logRowBackground = cx(styles.logsRow, { [styles.errorLogRow]: hasError, [styles.highlightBackground]: showingContext || permalinked, @@ -255,13 +256,22 @@ class UnThemedLogRow extends PureComponent { )} {hasError && ( )} + {isSampled && ( + + + + )} { width: 4em; cursor: default; `, - logIconError: css` - color: ${theme.colors.warning.main}; - `, + logIconError: css({ + color: theme.colors.warning.main, + position: 'relative', + top: '-2px', + }), + logIconInfo: css({ + color: theme.colors.info.main, + position: 'relative', + top: '-2px', + }), logsRowToggleDetails: css` label: logs-row-toggle-details__level; font-size: 9px; diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 150caee3740..4d8f88c5641 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -154,6 +154,22 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM }; }; +export const checkLogsSampled = (logRow: LogRowModel): { isSampled: boolean; sampleMessage?: string } => { + if (logRow.labels.__adaptive_logs_sampled__) { + let msg = + logRow.labels.__adaptive_logs_sampled__ === 'true' + ? 'Logs like this one have been dropped by Adaptive Logs' + : `${logRow.labels.__adaptive_logs_sampled__}% of logs like this one have been dropped by Adaptive Logs`; + return { + isSampled: true, + sampleMessage: msg, + }; + } + return { + isSampled: false, + }; +}; + export const escapeUnescapedString = (string: string) => string.replace(/\\r\\n|\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); From b0c043de5fe0779c89ed178995b864c07f935f39 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:06:35 +0100 Subject: [PATCH 005/122] Fix: Portuguese Brazilian wasn't loading translations (#89302) --- public/app/core/internationalization/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/internationalization/constants.ts b/public/app/core/internationalization/constants.ts index a241519fed4..f4ab99631cc 100644 --- a/public/app/core/internationalization/constants.ts +++ b/public/app/core/internationalization/constants.ts @@ -5,7 +5,7 @@ export const ENGLISH_US = 'en-US'; export const FRENCH_FRANCE = 'fr-FR'; export const SPANISH_SPAIN = 'es-ES'; export const GERMAN_GERMANY = 'de-DE'; -export const BRAZILIAN_PORTUGUESE = 'pt-br'; +export const BRAZILIAN_PORTUGUESE = 'pt-BR'; export const CHINESE_SIMPLIFIED = 'zh-Hans'; export const PSEUDO_LOCALE = 'pseudo-LOCALE'; From 9b7f9ae22e950ca6a6f15daf0c337d9d11d26ea5 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 18 Jun 2024 22:27:16 +0300 Subject: [PATCH 006/122] K8s: Rename origin.key to origin.hash (#89337) --- pkg/apimachinery/utils/meta.go | 20 +++++++++--------- pkg/apimachinery/utils/meta_test.go | 8 +++---- pkg/apiserver/rest/dualwriter_mode2.go | 21 +++---------------- .../apis/dashboard/access/sql_dashboards.go | 9 +++----- pkg/registry/apis/folders/conversions.go | 2 +- pkg/registry/apis/playlist/conversions.go | 4 ++-- .../apis/playlist/conversions_test.go | 2 +- .../apiserver/storage/entity/selector.go | 7 ------- .../apiserver/storage/entity/storage.go | 3 --- .../apiserver/storage/entity/utils.go | 6 +++--- pkg/services/store/entity/entity.proto | 2 +- pkg/tests/apis/helper.go | 7 +++++-- pkg/tests/apis/playlist/playlist_test.go | 2 +- 13 files changed, 34 insertions(+), 59 deletions(-) diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index ebc90171781..fa04df432c7 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -25,7 +25,7 @@ const AnnoKeySlug = "grafana.app/slug" const AnnoKeyOriginName = "grafana.app/originName" const AnnoKeyOriginPath = "grafana.app/originPath" -const AnnoKeyOriginKey = "grafana.app/originKey" +const AnnoKeyOriginHash = "grafana.app/originHash" const AnnoKeyOriginTimestamp = "grafana.app/originTimestamp" // ResourceOriginInfo is saved in annotations. This is used to identify where the resource came from @@ -37,8 +37,8 @@ type ResourceOriginInfo struct { // The path within the named origin above (external_id in the existing dashboard provisioing) Path string `json:"path,omitempty"` - // Verification/identification key (check_sum in existing dashboard provisioning) - Key string `json:"key,omitempty"` + // Verification/identification hash (check_sum in existing dashboard provisioning) + Hash string `json:"hash,omitempty"` // Origin modification timestamp when the resource was saved // This will be before the resource updated time @@ -77,7 +77,7 @@ type GrafanaMetaAccessor interface { SetOriginInfo(info *ResourceOriginInfo) GetOriginName() string GetOriginPath() string - GetOriginKey() string + GetOriginHash() string GetOriginTimestamp() (*time.Time, error) // Find a title in the object @@ -219,15 +219,15 @@ func (m *grafanaMetaAccessor) SetOriginInfo(info *ResourceOriginInfo) { delete(anno, AnnoKeyOriginName) delete(anno, AnnoKeyOriginPath) - delete(anno, AnnoKeyOriginKey) + delete(anno, AnnoKeyOriginHash) delete(anno, AnnoKeyOriginTimestamp) if info != nil && info.Name != "" { anno[AnnoKeyOriginName] = info.Name if info.Path != "" { anno[AnnoKeyOriginPath] = info.Path } - if info.Key != "" { - anno[AnnoKeyOriginKey] = info.Key + if info.Hash != "" { + anno[AnnoKeyOriginHash] = info.Hash } if info.Timestamp != nil { anno[AnnoKeyOriginTimestamp] = info.Timestamp.UTC().Format(time.RFC3339) @@ -245,7 +245,7 @@ func (m *grafanaMetaAccessor) GetOriginInfo() (*ResourceOriginInfo, error) { return &ResourceOriginInfo{ Name: v, Path: m.GetOriginPath(), - Key: m.GetOriginKey(), + Hash: m.GetOriginHash(), Timestamp: t, }, err } @@ -258,8 +258,8 @@ func (m *grafanaMetaAccessor) GetOriginPath() string { return m.get(AnnoKeyOriginPath) } -func (m *grafanaMetaAccessor) GetOriginKey() string { - return m.get(AnnoKeyOriginKey) +func (m *grafanaMetaAccessor) GetOriginHash() string { + return m.get(AnnoKeyOriginHash) } func (m *grafanaMetaAccessor) GetOriginTimestamp() (*time.Time, error) { diff --git a/pkg/apimachinery/utils/meta_test.go b/pkg/apimachinery/utils/meta_test.go index af8c0ed2c48..0e0b81db7f0 100644 --- a/pkg/apimachinery/utils/meta_test.go +++ b/pkg/apimachinery/utils/meta_test.go @@ -126,7 +126,7 @@ func TestMetaAccessor(t *testing.T) { originInfo := &utils.ResourceOriginInfo{ Name: "test", Path: "a/b/c", - Key: "kkk", + Hash: "kkk", } t.Run("fails for non resource objects", func(t *testing.T) { @@ -158,7 +158,7 @@ func TestMetaAccessor(t *testing.T) { require.Equal(t, map[string]string{ "grafana.app/originName": "test", "grafana.app/originPath": "a/b/c", - "grafana.app/originKey": "kkk", + "grafana.app/originHash": "kkk", "grafana.app/folder": "folderUID", }, res.GetAnnotations()) @@ -192,7 +192,7 @@ func TestMetaAccessor(t *testing.T) { require.Equal(t, map[string]string{ "grafana.app/originName": "test", "grafana.app/originPath": "a/b/c", - "grafana.app/originKey": "kkk", + "grafana.app/originHash": "kkk", "grafana.app/folder": "folderUID", }, obj.GetAnnotations()) @@ -215,7 +215,7 @@ func TestMetaAccessor(t *testing.T) { require.Equal(t, map[string]string{ "grafana.app/originName": "test", "grafana.app/originPath": "a/b/c", - "grafana.app/originKey": "kkk", + "grafana.app/originHash": "kkk", "grafana.app/folder": "folderUID", }, obj2.GetAnnotations()) diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index 12e4703d829..c63c7fbb3ee 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -4,16 +4,15 @@ import ( "context" "time" - "github.com/grafana/grafana/pkg/apimachinery/utils" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/selection" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/klog/v2" + + "github.com/grafana/grafana/pkg/apimachinery/utils" ) type DualWriterMode2 struct { @@ -317,29 +316,15 @@ func parseList(legacyList []runtime.Object) (metainternalversion.ListOptions, ma indexMap := map[string]int{} for i, obj := range legacyList { - metaAccessor, err := utils.MetaAccessor(obj) - if err != nil { - return options, nil, err - } - originKeys = append(originKeys, metaAccessor.GetOriginKey()) - - accessor, err := meta.Accessor(obj) + accessor, err := utils.MetaAccessor(obj) if err != nil { return options, nil, err } indexMap[accessor.GetName()] = i } - if len(originKeys) == 0 { return options, nil, nil } - - r, err := labels.NewRequirement(utils.AnnoKeyOriginKey, selection.In, originKeys) - if err != nil { - return options, nil, err - } - options.LabelSelector = labels.NewSelector().Add(*r) - return options, indexMap, nil } diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/access/sql_dashboards.go index 13db6037eb5..f043e84b9fb 100644 --- a/pkg/registry/apis/dashboard/access/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/access/sql_dashboards.go @@ -100,9 +100,6 @@ func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery, if query.Requirements.ListDeleted { return nil, 0, fmt.Errorf("ListDeleted not yet supported") } - if len(query.Requirements.ListOriginKeys) > 0 { - return nil, 0, fmt.Errorf("ListOriginKeys not yet supported") - } token, err := readContinueToken(query) if err != nil { @@ -318,7 +315,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { var origin_name sql.NullString var origin_path sql.NullString var origin_ts sql.NullInt64 - var origin_key sql.NullString + var origin_hash sql.NullString var data []byte // the dashboard JSON var version int64 @@ -327,7 +324,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { &created, &createdByID, &createdByName, &updated, &updatedByID, &updatedByName, &plugin_id, - &origin_name, &origin_path, &origin_key, &origin_ts, + &origin_name, &origin_path, &origin_hash, &origin_ts, &version, &row.Title, &data, ) @@ -370,7 +367,7 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: origin_name.String, Path: originPath, - Key: origin_key.String, + Hash: origin_hash.String, Timestamp: &ts, }) } else if plugin_id != "" { diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 1c4b8435379..d7f2afe02c6 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -33,7 +33,7 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) if v.ID > 0 { // nolint:staticcheck meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: "SQL", - Key: fmt.Sprintf("%d", v.ID), // nolint:staticcheck + Path: fmt.Sprintf("%d", v.ID), // nolint:staticcheck }) } if v.CreatedBy > 0 { diff --git a/pkg/registry/apis/playlist/conversions.go b/pkg/registry/apis/playlist/conversions.go index a5a00313157..89a8616cfe1 100644 --- a/pkg/registry/apis/playlist/conversions.go +++ b/pkg/registry/apis/playlist/conversions.go @@ -95,7 +95,7 @@ func convertToK8sResource(v *playlistsvc.PlaylistDTO, namespacer request.Namespa createdAt := time.UnixMilli(v.CreatedAt).UTC() meta.SetOriginInfo(&utils.ResourceOriginInfo{ Name: "SQL", - Key: fmt.Sprintf("%d", v.Id), + Path: fmt.Sprintf("%d", v.Id), Timestamp: &createdAt, }) } @@ -133,7 +133,7 @@ func getLegacyID(item *unstructured.Unstructured) int64 { } info, _ := meta.GetOriginInfo() if info != nil && info.Name == "SQL" { - i, err := strconv.ParseInt(info.Key, 10, 64) + i, err := strconv.ParseInt(info.Path, 10, 64) if err == nil { return i } diff --git a/pkg/registry/apis/playlist/conversions_test.go b/pkg/registry/apis/playlist/conversions_test.go index d95ae123e8f..6c42cd40d0b 100644 --- a/pkg/registry/apis/playlist/conversions_test.go +++ b/pkg/registry/apis/playlist/conversions_test.go @@ -42,7 +42,7 @@ func TestPlaylistConversion(t *testing.T) { "resourceVersion": "54321", "creationTimestamp": "1970-01-01T00:00:12Z", "annotations": { - "grafana.app/originKey": "123", + "grafana.app/originPath": "123", "grafana.app/originName": "SQL", "grafana.app/originTimestamp":"1970-01-01T00:00:12Z", "grafana.app/updatedTimestamp": "1970-01-01T00:00:54Z" diff --git a/pkg/services/apiserver/storage/entity/selector.go b/pkg/services/apiserver/storage/entity/selector.go index 7bd946a60c7..a67ab4bc2b5 100644 --- a/pkg/services/apiserver/storage/entity/selector.go +++ b/pkg/services/apiserver/storage/entity/selector.go @@ -21,8 +21,6 @@ type Requirements struct { ListDeleted bool // ListHistory is a resource name to list the history of ListHistory string - // ListOriginKeys needs to include the origin key of a given entity in order for it to be selected. - ListOriginKeys []string } func ReadLabelSelectors(selector labels.Selector) (Requirements, labels.Selector, error) { @@ -48,11 +46,6 @@ func ReadLabelSelectors(selector labels.Selector) (Requirements, labels.Selector return requirements, newSelector, apierrors.NewBadRequest(SortByKey + " label selector only supports in") } requirements.SortBy = r.Values().List() - case utils.AnnoKeyOriginKey: - if r.Operator() != selection.In { - return requirements, newSelector, apierrors.NewBadRequest(SortByKey + " label selector only supports in") - } - requirements.ListOriginKeys = r.Values().List() case ListDeletedKey: if r.Operator() != selection.Equals { return requirements, newSelector, apierrors.NewBadRequest(ListDeletedKey + " label selector only supports equality") diff --git a/pkg/services/apiserver/storage/entity/storage.go b/pkg/services/apiserver/storage/entity/storage.go index 67884541b1e..d971f3a9b2a 100644 --- a/pkg/services/apiserver/storage/entity/storage.go +++ b/pkg/services/apiserver/storage/entity/storage.go @@ -442,9 +442,6 @@ func (s *Storage) GetList(ctx context.Context, key string, opts storage.ListOpti if len(requirements.SortBy) > 0 { req.Sort = requirements.SortBy } - if len(requirements.ListOriginKeys) > 0 { - req.OriginKeys = requirements.ListOriginKeys - } if requirements.ListDeleted { req.Deleted = true } diff --git a/pkg/services/apiserver/storage/entity/utils.go b/pkg/services/apiserver/storage/entity/utils.go index c7f854297ca..732a455750a 100644 --- a/pkg/services/apiserver/storage/entity/utils.go +++ b/pkg/services/apiserver/storage/entity/utils.go @@ -73,7 +73,7 @@ func entityToResource(rsp *entityStore.Entity, res runtime.Object, codec runtime originTime := time.UnixMilli(rsp.Origin.Time).UTC() grafanaAccessor.SetOriginInfo(&utils.ResourceOriginInfo{ Name: rsp.Origin.Source, - Key: rsp.Origin.Key, + Path: rsp.Origin.Key, // Using "key" in the // Path: rsp.Origin.Path, Timestamp: &originTime, }) @@ -136,8 +136,8 @@ func resourceToEntity(res runtime.Object, requestInfo *request.RequestInfo, code Title: grafanaAccessor.FindTitle(metaAccessor.GetName()), Origin: &entityStore.EntityOriginInfo{ Source: grafanaAccessor.GetOriginName(), - Key: grafanaAccessor.GetOriginKey(), - // Path: grafanaAccessor.GetOriginPath(), + // Deprecated: Keeping "key" in the protobuf to avoid migrations while a bigger one is in place + Key: grafanaAccessor.GetOriginPath(), }, Labels: metaAccessor.GetLabels(), } diff --git a/pkg/services/store/entity/entity.proto b/pkg/services/store/entity/entity.proto index 9e9a4ec66d3..087b6e5384d 100644 --- a/pkg/services/store/entity/entity.proto +++ b/pkg/services/store/entity/entity.proto @@ -311,7 +311,7 @@ message EntityListRequest { // list deleted entities instead of active ones bool deleted = 12; - // Limit to a set of origin keys (empty is all) + // Deprecated: Limit to a set of origin keys (empty is all) repeated string origin_keys = 13; } diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index ad0bef49eb6..c1e1529b2b6 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -143,8 +143,11 @@ func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string { deep := v.DeepCopy() anno := deep.GetAnnotations() - if anno["grafana.app/originKey"] != "" { - anno["grafana.app/originKey"] = "${originKey}" + if anno["grafana.app/originPath"] != "" { + anno["grafana.app/originPath"] = "${originPath}" + } + if anno["grafana.app/originHash"] != "" { + anno["grafana.app/originHash"] = "${originHash}" } if anno["grafana.app/updatedTimestamp"] != "" { anno["grafana.app/updatedTimestamp"] = "${updatedTimestamp}" diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index a8c359959df..fd4a0939732 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -360,7 +360,7 @@ func doPlaylistTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelp "kind": "Playlist", "metadata": { "annotations": { - "grafana.app/originKey": "${originKey}", + "grafana.app/originPath": "${originPath}", "grafana.app/originName": "SQL", "grafana.app/updatedTimestamp": "${updatedTimestamp}" }, From 3bbc821131f1b10ace139dbb4a6880fb77686646 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Tue, 18 Jun 2024 15:37:44 -0400 Subject: [PATCH 007/122] Metrics: Add ability to disable classic histogram for HTTP metric (#88315) metrics: Add ability to disable classic histogram for HTTP metric Signed-off-by: Dave Henderson --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/middleware/request_metrics.go | 11 +- pkg/services/featuremgmt/registry.go | 23 +- pkg/services/featuremgmt/toggles_gen.csv | 3 +- pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 417 +++++++++--------- 7 files changed, 236 insertions(+), 224 deletions(-) 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 53578a6dfb7..fc6e4bd12ad 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -151,6 +151,7 @@ Experimental features might be changed or removed without prior notice. | `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | | `idForwarding` | Generate signed id token for identity that can be forwarded to plugins and external services | | `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 | | `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7f5da38828d..5bb2f23c949 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -112,6 +112,7 @@ export interface FeatureToggles { externalServiceAccounts?: boolean; panelMonitoring?: boolean; enableNativeHTTPHistogram?: boolean; + disableClassicHTTPHistogram?: boolean; formatString?: boolean; transformationsVariableSupport?: boolean; kubernetesPlaylists?: boolean; diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index b109a52a56f..94c60f57509 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -51,12 +51,19 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR if features.IsEnabledGlobally(featuremgmt.FlagEnableNativeHTTPHistogram) { // the recommended default value from the prom_client // https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L411 - // Giving this variable an value means the client will expose the histograms as an - // native histogram instead of normal a normal histogram. + // Giving this variable a value means the client will expose a native + // histogram. histogramOptions.NativeHistogramBucketFactor = 1.1 // The default value in OTel. It probably good enough for us as well. histogramOptions.NativeHistogramMaxBucketNumber = 160 histogramOptions.NativeHistogramMinResetDuration = time.Hour + + if features.IsEnabledGlobally(featuremgmt.FlagDisableClassicHTTPHistogram) { + // setting Buckets to nil with native options set means the classic + // histogram will no longer be exposed - this can be a good way to + // reduce cardinality in the exposed metrics + histogramOptions.Buckets = nil + } } httpRequestDurationHistogram := prometheus.NewHistogramVec( diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 51bf6ea8828..8842445b5a6 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -700,11 +700,24 @@ var ( FrontendOnly: true, }, { - Name: "enableNativeHTTPHistogram", - Description: "Enables native HTTP Histograms", - Stage: FeatureStageExperimental, - FrontendOnly: false, - Owner: hostedGrafanaTeam, + Name: "enableNativeHTTPHistogram", + Description: "Enables native HTTP Histograms", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaBackendServicesSquad, + HideFromAdminPage: true, + AllowSelfServe: false, + RequiresRestart: true, + }, + FeatureFlag{ + Name: "disableClassicHTTPHistogram", + Description: "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaBackendServicesSquad, + HideFromAdminPage: true, + AllowSelfServe: false, + RequiresRestart: true, }, { Name: "formatString", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c0b5e76d4c1..969586eee2b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -92,7 +92,8 @@ pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,tru idForwarding,experimental,@grafana/identity-access-team,false,false,false externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false panelMonitoring,GA,@grafana/dataviz-squad,false,false,true -enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false +enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false +disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false formatString,preview,@grafana/dataviz-squad,false,false,true transformationsVariableSupport,GA,@grafana/dataviz-squad,false,false,true kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index d7f10257ddf..45d8a4f2380 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -383,6 +383,10 @@ const ( // Enables native HTTP Histograms FlagEnableNativeHTTPHistogram = "enableNativeHTTPHistogram" + // FlagDisableClassicHTTPHistogram + // Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) + FlagDisableClassicHTTPHistogram = "disableClassicHTTPHistogram" + // FlagFormatString // Enable format string transformer FlagFormatString = "formatString" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 02df39301fd..bde6f3022f7 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -6,7 +6,7 @@ { "metadata": { "name": "accessActionSets", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-12T16:19:25Z" }, "spec": { @@ -18,7 +18,7 @@ { "metadata": { "name": "accessControlOnCall", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-10-19T16:10:09Z" }, "spec": { @@ -31,7 +31,7 @@ { "metadata": { "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-03T14:39:58Z" }, "spec": { @@ -44,7 +44,7 @@ { "metadata": { "name": "aiGeneratedDashboardChanges", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-03-05T12:01:31Z" }, "spec": { @@ -57,7 +57,7 @@ { "metadata": { "name": "alertStateHistoryLokiOnly", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-30T18:53:21Z" }, "spec": { @@ -69,7 +69,7 @@ { "metadata": { "name": "alertStateHistoryLokiPrimary", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-30T18:53:21Z" }, "spec": { @@ -81,7 +81,7 @@ { "metadata": { "name": "alertStateHistoryLokiSecondary", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-30T18:53:21Z" }, "spec": { @@ -93,7 +93,7 @@ { "metadata": { "name": "alertingBacktesting", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-12-14T14:44:14Z" }, "spec": { @@ -105,7 +105,7 @@ { "metadata": { "name": "alertingCentralAlertHistory", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-29T15:01:38Z" }, "spec": { @@ -118,7 +118,7 @@ { "metadata": { "name": "alertingDisableSendAlertsExternal", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-23T12:29:19Z" }, "spec": { @@ -132,7 +132,7 @@ { "metadata": { "name": "alertingInsights", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-14T12:58:04Z" }, "spec": { @@ -146,7 +146,7 @@ { "metadata": { "name": "alertingListViewV2", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-24T14:40:49Z" }, "spec": { @@ -159,7 +159,7 @@ { "metadata": { "name": "alertingNoDataErrorExecution", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-08-15T14:27:15Z" }, "spec": { @@ -172,7 +172,7 @@ { "metadata": { "name": "alertingNoNormalState", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-01-13T23:29:29Z" }, "spec": { @@ -185,7 +185,7 @@ { "metadata": { "name": "alertingQueryOptimization", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-10T20:52:58Z" }, "spec": { @@ -197,7 +197,7 @@ { "metadata": { "name": "alertingSaveStatePeriodic", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-23T16:03:30Z" }, "spec": { @@ -209,7 +209,7 @@ { "metadata": { "name": "alertingSimplifiedRouting", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-10T13:14:39Z" }, "spec": { @@ -221,7 +221,7 @@ { "metadata": { "name": "alertmanagerRemoteOnly", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -233,7 +233,7 @@ { "metadata": { "name": "alertmanagerRemotePrimary", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -245,7 +245,7 @@ { "metadata": { "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -257,7 +257,7 @@ { "metadata": { "name": "angularDeprecationUI", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-08-29T14:05:47Z" }, "spec": { @@ -270,7 +270,7 @@ { "metadata": { "name": "annotationPermissionUpdate", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-31T13:30:13Z" }, "spec": { @@ -282,7 +282,7 @@ { "metadata": { "name": "authAPIAccessTokenAuth", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-02T15:45:15Z" }, "spec": { @@ -296,8 +296,8 @@ { "metadata": { "name": "authZGRPCServer", - "resourceVersion": "1718093439898", - "creationTimestamp": "2024-06-11T08:10:39Z" + "resourceVersion": "1718727528075", + "creationTimestamp": "2024-06-18T16:18:48Z" }, "spec": { "description": "Enables the gRPC server for authorization", @@ -310,7 +310,7 @@ { "metadata": { "name": "autoMigrateGraphPanel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-08T22:00:48Z" }, "spec": { @@ -323,7 +323,7 @@ { "metadata": { "name": "autoMigrateOldPanels", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-23T04:02:36Z" }, "spec": { @@ -336,7 +336,7 @@ { "metadata": { "name": "autoMigratePiechartPanel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-14T16:06:25Z" }, "spec": { @@ -349,7 +349,7 @@ { "metadata": { "name": "autoMigrateStatPanel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-14T16:06:25Z" }, "spec": { @@ -362,7 +362,7 @@ { "metadata": { "name": "autoMigrateTablePanel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-14T16:06:25Z" }, "spec": { @@ -375,7 +375,7 @@ { "metadata": { "name": "autoMigrateWorldmapPanel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-14T16:06:25Z" }, "spec": { @@ -388,7 +388,7 @@ { "metadata": { "name": "autoMigrateXYChartPanel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-03-22T15:44:37Z" }, "spec": { @@ -401,7 +401,7 @@ { "metadata": { "name": "autofixDSUID", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-03T11:32:07Z" }, "spec": { @@ -413,7 +413,7 @@ { "metadata": { "name": "awsAsyncQueryCaching", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-21T15:34:07Z" }, "spec": { @@ -425,7 +425,7 @@ { "metadata": { "name": "awsDatasourcesNewFormStyling", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-12T08:59:10Z" }, "spec": { @@ -438,7 +438,7 @@ { "metadata": { "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-06T15:06:11Z" }, "spec": { @@ -450,7 +450,7 @@ { "metadata": { "name": "azureMonitorPrometheusExemplars", - "resourceVersion": "1717667267324", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-06-06T16:53:17Z" }, "spec": { @@ -459,24 +459,10 @@ "codeowner": "@grafana/partner-datasources" } }, - { - "metadata": { - "name": "betterPageScrolling", - "resourceVersion": "1717578796182", - "creationTimestamp": "2024-03-06T15:06:47Z", - "deletionTimestamp": "2024-06-18T09:25:56Z" - }, - "spec": { - "description": "Removes CustomScrollbar from the UI, relying on native browser scrollbars", - "stage": "GA", - "codeowner": "@grafana/grafana-frontend-platform", - "frontend": true - } - }, { "metadata": { "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-12T16:56:49Z" }, "spec": { @@ -488,7 +474,7 @@ { "metadata": { "name": "canvasPanelNesting", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-05-31T19:03:34Z" }, "spec": { @@ -502,7 +488,7 @@ { "metadata": { "name": "canvasPanelPanZoom", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-02T19:52:21Z" }, "spec": { @@ -515,7 +501,7 @@ { "metadata": { "name": "cloudRBACRoles", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-10T13:19:01Z" }, "spec": { @@ -529,7 +515,7 @@ { "metadata": { "name": "cloudWatchBatchQueries", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-20T19:09:41Z" }, "spec": { @@ -541,7 +527,7 @@ { "metadata": { "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-11-28T11:39:12Z" }, "spec": { @@ -554,7 +540,7 @@ { "metadata": { "name": "cloudWatchNewLabelParsing", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-05T15:57:56Z" }, "spec": { @@ -566,7 +552,7 @@ { "metadata": { "name": "configurableSchedulerTick", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-26T16:44:12Z" }, "spec": { @@ -580,7 +566,7 @@ { "metadata": { "name": "correlations", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-09-16T13:14:27Z" }, "spec": { @@ -593,7 +579,7 @@ { "metadata": { "name": "dashboardRestore", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-16T17:36:26Z" }, "spec": { @@ -606,7 +592,7 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-13T08:51:21Z" }, "spec": { @@ -619,7 +605,7 @@ { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-02T19:02:25Z" }, "spec": { @@ -632,7 +618,7 @@ { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-11T08:08:47Z" }, "spec": { @@ -645,7 +631,7 @@ { "metadata": { "name": "dashgpt", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-08-30T20:22:05Z" }, "spec": { @@ -658,8 +644,8 @@ { "metadata": { "name": "databaseReadReplica", - "resourceVersion": "1718308641844", - "creationTimestamp": "2024-06-13T19:57:21Z" + "resourceVersion": "1718727528075", + "creationTimestamp": "2024-06-18T16:18:48Z" }, "spec": { "description": "Use a read replica for some database queries.", @@ -670,7 +656,7 @@ { "metadata": { "name": "dataplaneFrontendFallback", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-04-07T21:13:19Z" }, "spec": { @@ -684,7 +670,7 @@ { "metadata": { "name": "datasourceProxyDisableRBAC", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-21T13:05:16Z" }, "spec": { @@ -697,7 +683,7 @@ { "metadata": { "name": "datasourceQueryMultiStatus", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-05-03T16:02:20Z" }, "spec": { @@ -709,7 +695,7 @@ { "metadata": { "name": "datasourceQueryTypes", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-23T16:46:28Z" }, "spec": { @@ -722,7 +708,7 @@ { "metadata": { "name": "disableAngular", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-23T15:43:45Z" }, "spec": { @@ -733,10 +719,24 @@ "hideFromAdminPage": true } }, + { + "metadata": { + "name": "disableClassicHTTPHistogram", + "resourceVersion": "1718727528075", + "creationTimestamp": "2024-06-18T16:18:48Z" + }, + "spec": { + "description": "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)", + "stage": "experimental", + "codeowner": "@grafana/grafana-backend-services-squad", + "requiresRestart": true, + "hideFromAdminPage": true + } + }, { "metadata": { "name": "disableEnvelopeEncryption", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-05-24T08:34:47Z" }, "spec": { @@ -749,7 +749,7 @@ { "metadata": { "name": "disableNumericMetricsSortingInExpressions", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-16T14:52:47Z" }, "spec": { @@ -762,7 +762,7 @@ { "metadata": { "name": "disableSSEDataplane", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-04-12T16:24:34Z" }, "spec": { @@ -774,7 +774,7 @@ { "metadata": { "name": "disableSecretsCompatibility", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-07-12T20:27:37Z" }, "spec": { @@ -787,7 +787,7 @@ { "metadata": { "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-01-24T09:43:44Z" }, "spec": { @@ -800,7 +800,7 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-04-24T14:46:31Z" }, "spec": { @@ -810,36 +810,24 @@ "frontend": true } }, - { - "metadata": { - "name": "enableElasticsearchBackendQuerying", - "resourceVersion": "1717578796182", - "creationTimestamp": "2023-04-14T09:24:35Z", - "deletionTimestamp": "2024-06-05T15:03:29Z" - }, - "spec": { - "description": "Enable the processing of queries and responses in the Elasticsearch data source through backend", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "allowSelfServe": true - } - }, { "metadata": { "name": "enableNativeHTTPHistogram", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-03T18:23:55Z" }, "spec": { "description": "Enables native HTTP Histograms", "stage": "experimental", - "codeowner": "@grafana/hosted-grafana-team" + "codeowner": "@grafana/grafana-backend-services-squad", + "requiresRestart": true, + "hideFromAdminPage": true } }, { "metadata": { "name": "exploreContentOutline", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-13T16:57:13Z" }, "spec": { @@ -853,7 +841,7 @@ { "metadata": { "name": "exploreMetrics", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-09T18:15:18Z" }, "spec": { @@ -866,7 +854,7 @@ { "metadata": { "name": "expressionParser", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-17T00:59:11Z" }, "spec": { @@ -879,7 +867,7 @@ { "metadata": { "name": "externalCorePlugins", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-22T08:50:13Z" }, "spec": { @@ -891,7 +879,7 @@ { "metadata": { "name": "externalServiceAccounts", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-28T07:26:37Z" }, "spec": { @@ -904,7 +892,7 @@ { "metadata": { "name": "extraThemes", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-05-10T13:37:04Z" }, "spec": { @@ -917,7 +905,7 @@ { "metadata": { "name": "extractFieldsNameDeduplication", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-02T15:47:42Z" }, "spec": { @@ -930,7 +918,7 @@ { "metadata": { "name": "faroDatasourceSelector", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-05-05T00:35:10Z" }, "spec": { @@ -943,7 +931,7 @@ { "metadata": { "name": "featureHighlights", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-02-03T11:53:23Z" }, "spec": { @@ -956,7 +944,7 @@ { "metadata": { "name": "featureToggleAdminPage", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-18T20:43:32Z" }, "spec": { @@ -970,7 +958,7 @@ { "metadata": { "name": "flameGraphItemCollapsing", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-09T14:31:07Z" }, "spec": { @@ -983,7 +971,7 @@ { "metadata": { "name": "formatString", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-13T18:17:12Z" }, "spec": { @@ -996,7 +984,7 @@ { "metadata": { "name": "frontendSandboxMonitorOnly", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-05T11:48:25Z" }, "spec": { @@ -1009,7 +997,7 @@ { "metadata": { "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-12-06T20:21:21Z" }, "spec": { @@ -1023,7 +1011,7 @@ { "metadata": { "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-06T18:55:22Z" }, "spec": { @@ -1037,7 +1025,7 @@ { "metadata": { "name": "grafanaManagedRecordingRules", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-22T17:53:16Z" }, "spec": { @@ -1051,7 +1039,7 @@ { "metadata": { "name": "groupByVariable", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-14T17:18:04Z" }, "spec": { @@ -1065,7 +1053,7 @@ { "metadata": { "name": "groupToNestedTableTransformation", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-07T14:28:26Z" }, "spec": { @@ -1078,7 +1066,7 @@ { "metadata": { "name": "grpcServer", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-09-26T20:25:34Z" }, "spec": { @@ -1091,7 +1079,7 @@ { "metadata": { "name": "idForwarding", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-25T15:21:28Z" }, "spec": { @@ -1103,7 +1091,7 @@ { "metadata": { "name": "individualCookiePreferences", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-02-21T10:19:07Z" }, "spec": { @@ -1115,7 +1103,7 @@ { "metadata": { "name": "influxdbBackendMigration", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-02-09T18:26:16Z", "deletionTimestamp": "2023-01-17T14:11:26Z" }, @@ -1129,7 +1117,7 @@ { "metadata": { "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-01T10:58:24Z" }, "spec": { @@ -1141,7 +1129,7 @@ { "metadata": { "name": "influxqlStreamingParser", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-29T17:29:35Z" }, "spec": { @@ -1153,7 +1141,7 @@ { "metadata": { "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-18T18:48:11Z" }, "spec": { @@ -1167,7 +1155,7 @@ { "metadata": { "name": "kubernetesAggregator", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-12T20:59:35Z" }, "spec": { @@ -1180,7 +1168,7 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1717593661635", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-06-05T14:34:23Z" }, "spec": { @@ -1193,7 +1181,7 @@ { "metadata": { "name": "kubernetesFeatureToggles", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-18T05:32:44Z" }, "spec": { @@ -1207,7 +1195,7 @@ { "metadata": { "name": "kubernetesPlaylists", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-05T19:00:36Z" }, "spec": { @@ -1220,7 +1208,7 @@ { "metadata": { "name": "kubernetesSnapshots", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-12-05T22:31:49Z" }, "spec": { @@ -1233,7 +1221,7 @@ { "metadata": { "name": "libraryPanelRBAC", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-11T23:30:50Z" }, "spec": { @@ -1246,7 +1234,7 @@ { "metadata": { "name": "live-service-web-worker", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-01-26T17:44:20Z" }, "spec": { @@ -1259,7 +1247,7 @@ { "metadata": { "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-06-10T08:56:55Z" }, "spec": { @@ -1271,7 +1259,7 @@ { "metadata": { "name": "logRowsPopoverMenu", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-16T09:48:10Z" }, "spec": { @@ -1284,7 +1272,7 @@ { "metadata": { "name": "logsContextDatasourceUi", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-01-27T14:12:01Z" }, "spec": { @@ -1298,7 +1286,7 @@ { "metadata": { "name": "logsExploreTableDefaultVisualization", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-02T15:28:15Z" }, "spec": { @@ -1311,7 +1299,7 @@ { "metadata": { "name": "logsExploreTableVisualisation", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-12T13:52:42Z" }, "spec": { @@ -1324,7 +1312,7 @@ { "metadata": { "name": "logsInfiniteScrolling", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-09T10:54:03Z" }, "spec": { @@ -1337,7 +1325,7 @@ { "metadata": { "name": "lokiExperimentalStreaming", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-19T10:03:51Z" }, "spec": { @@ -1349,7 +1337,7 @@ { "metadata": { "name": "lokiLogsDataplane", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-13T07:58:00Z" }, "spec": { @@ -1361,7 +1349,7 @@ { "metadata": { "name": "lokiMetricDataplane", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-04-13T13:07:08Z" }, "spec": { @@ -1374,7 +1362,7 @@ { "metadata": { "name": "lokiPredefinedOperations", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-02T10:52:36Z" }, "spec": { @@ -1387,7 +1375,7 @@ { "metadata": { "name": "lokiQueryHints", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-12-18T20:43:16Z" }, "spec": { @@ -1400,7 +1388,7 @@ { "metadata": { "name": "lokiQuerySplitting", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-02-09T17:27:02Z" }, "spec": { @@ -1414,7 +1402,7 @@ { "metadata": { "name": "lokiQuerySplittingConfig", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-20T15:51:36Z" }, "spec": { @@ -1427,7 +1415,7 @@ { "metadata": { "name": "lokiRunQueriesInParallel", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-19T09:34:01Z" }, "spec": { @@ -1439,7 +1427,7 @@ { "metadata": { "name": "lokiStructuredMetadata", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-16T16:06:14Z" }, "spec": { @@ -1451,7 +1439,7 @@ { "metadata": { "name": "managedPluginsInstall", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-18T13:17:03Z" }, "spec": { @@ -1463,7 +1451,7 @@ { "metadata": { "name": "metricsSummary", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-08-28T14:02:12Z" }, "spec": { @@ -1476,7 +1464,7 @@ { "metadata": { "name": "mlExpressions", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-13T17:37:50Z" }, "spec": { @@ -1488,7 +1476,7 @@ { "metadata": { "name": "mysqlAnsiQuotes", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-10-12T11:43:35Z" }, "spec": { @@ -1500,7 +1488,7 @@ { "metadata": { "name": "nestedFolders", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-10-26T14:15:14Z" }, "spec": { @@ -1512,7 +1500,7 @@ { "metadata": { "name": "newDashboardSharingComponent", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-03T15:02:18Z" }, "spec": { @@ -1525,7 +1513,7 @@ { "metadata": { "name": "newDashboardWithFiltersAndGroupBy", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-04T11:25:21Z" }, "spec": { @@ -1539,7 +1527,7 @@ { "metadata": { "name": "newFolderPicker", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-15T11:43:19Z" }, "spec": { @@ -1552,7 +1540,7 @@ { "metadata": { "name": "newPDFRendering", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-08T12:09:34Z" }, "spec": { @@ -1564,7 +1552,7 @@ { "metadata": { "name": "nodeGraphDotLayout", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-31T16:26:12Z" }, "spec": { @@ -1577,7 +1565,7 @@ { "metadata": { "name": "notificationBanner", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-13T09:32:34Z" }, "spec": { @@ -1589,7 +1577,7 @@ { "metadata": { "name": "oauthRequireSubClaim", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-03-25T13:22:24Z" }, "spec": { @@ -1603,7 +1591,7 @@ { "metadata": { "name": "onPremToCloudMigrations", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-22T16:09:08Z" }, "spec": { @@ -1615,8 +1603,8 @@ { "metadata": { "name": "openSearchBackendFlowEnabled", - "resourceVersion": "1718357852240", - "creationTimestamp": "2024-06-14T09:37:32Z" + "resourceVersion": "1718727528075", + "creationTimestamp": "2024-06-18T16:18:48Z" }, "spec": { "description": "Enables the backend query flow for Open Search datasource plugin", @@ -1627,7 +1615,7 @@ { "metadata": { "name": "panelFilterVariable", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-03T12:15:54Z" }, "spec": { @@ -1641,7 +1629,7 @@ { "metadata": { "name": "panelMonitoring", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-09T05:19:08Z" }, "spec": { @@ -1654,7 +1642,7 @@ { "metadata": { "name": "panelTitleSearch", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-02-15T18:26:03Z" }, "spec": { @@ -1667,7 +1655,7 @@ { "metadata": { "name": "panelTitleSearchInV1", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-13T12:04:24Z" }, "spec": { @@ -1680,7 +1668,7 @@ { "metadata": { "name": "pdfTables", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-06T13:39:22Z" }, "spec": { @@ -1692,7 +1680,7 @@ { "metadata": { "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-08-02T07:39:25Z" }, "spec": { @@ -1704,8 +1692,8 @@ { "metadata": { "name": "pinNavItems", - "resourceVersion": "1718017263521", - "creationTimestamp": "2024-06-10T11:01:03Z" + "resourceVersion": "1718727528075", + "creationTimestamp": "2024-06-18T16:18:48Z" }, "spec": { "description": "Enables pinning of nav items", @@ -1716,7 +1704,7 @@ { "metadata": { "name": "pluginProxyPreserveTrailingSlash", - "resourceVersion": "1717581171624", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-06-05T11:36:14Z" }, "spec": { @@ -1728,7 +1716,7 @@ { "metadata": { "name": "pluginsAPIMetrics", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-21T11:36:32Z" }, "spec": { @@ -1741,7 +1729,7 @@ { "metadata": { "name": "pluginsFrontendSandbox", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-05T08:51:36Z" }, "spec": { @@ -1754,7 +1742,7 @@ { "metadata": { "name": "pluginsSkipHostEnvVars", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-15T17:09:14Z" }, "spec": { @@ -1766,7 +1754,7 @@ { "metadata": { "name": "preserveDashboardStateWhenNavigating", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-27T12:28:06Z" }, "spec": { @@ -1780,7 +1768,7 @@ { "metadata": { "name": "promQLScope", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-01-29T20:22:17Z" }, "spec": { @@ -1792,7 +1780,7 @@ { "metadata": { "name": "prometheusCodeModeMetricNamesSearch", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-04T20:38:23Z" }, "spec": { @@ -1805,7 +1793,7 @@ { "metadata": { "name": "prometheusConfigOverhaulAuth", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-26T16:09:53Z" }, "spec": { @@ -1817,7 +1805,7 @@ { "metadata": { "name": "prometheusDataplane", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-29T15:26:32Z" }, "spec": { @@ -1830,7 +1818,7 @@ { "metadata": { "name": "prometheusIncrementalQueryInstrumentation", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-05T19:39:49Z" }, "spec": { @@ -1843,7 +1831,7 @@ { "metadata": { "name": "prometheusMetricEncyclopedia", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-07T18:41:05Z" }, "spec": { @@ -1857,7 +1845,7 @@ { "metadata": { "name": "prometheusPromQAIL", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-19T15:45:32Z" }, "spec": { @@ -1870,7 +1858,7 @@ { "metadata": { "name": "publicDashboards", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-04-07T18:30:19Z" }, "spec": { @@ -1883,7 +1871,7 @@ { "metadata": { "name": "publicDashboardsEmailSharing", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-01-03T19:45:15Z" }, "spec": { @@ -1897,7 +1885,7 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-03-22T14:48:21Z" }, "spec": { @@ -1910,7 +1898,7 @@ { "metadata": { "name": "queryLibrary", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-10-07T18:31:45Z", "deletionTimestamp": "2023-03-20T16:00:14Z" }, @@ -1923,7 +1911,7 @@ { "metadata": { "name": "queryOverLive", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-01-26T17:44:20Z" }, "spec": { @@ -1936,7 +1924,7 @@ { "metadata": { "name": "queryService", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -1949,7 +1937,7 @@ { "metadata": { "name": "queryServiceFromUI", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -1962,7 +1950,7 @@ { "metadata": { "name": "queryServiceRewrite", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -1975,7 +1963,7 @@ { "metadata": { "name": "recordedQueriesMulti", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-14T12:34:22Z" }, "spec": { @@ -1987,7 +1975,7 @@ { "metadata": { "name": "recoveryThreshold", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-10T14:51:50Z" }, "spec": { @@ -2000,7 +1988,7 @@ { "metadata": { "name": "refactorVariablesTimeRange", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-06T13:12:09Z" }, "spec": { @@ -2013,7 +2001,7 @@ { "metadata": { "name": "regressionTransformation", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-24T14:49:16Z" }, "spec": { @@ -2026,7 +2014,7 @@ { "metadata": { "name": "renderAuthJWT", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-04-03T16:53:38Z" }, "spec": { @@ -2039,7 +2027,7 @@ { "metadata": { "name": "reportingRetries", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-08-31T07:47:47Z" }, "spec": { @@ -2052,7 +2040,7 @@ { "metadata": { "name": "scenes", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-07-07T06:53:02Z" }, "spec": { @@ -2065,7 +2053,7 @@ { "metadata": { "name": "scopeFilters", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-03-05T15:41:19Z" }, "spec": { @@ -2079,7 +2067,7 @@ { "metadata": { "name": "showDashboardValidationWarnings", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-10-14T13:51:05Z" }, "spec": { @@ -2091,7 +2079,7 @@ { "metadata": { "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-06T16:28:52Z" }, "spec": { @@ -2105,7 +2093,7 @@ { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-02-27T21:16:00Z" }, "spec": { @@ -2117,7 +2105,7 @@ { "metadata": { "name": "sseGroupByDatasource", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-07T20:02:07Z" }, "spec": { @@ -2129,7 +2117,7 @@ { "metadata": { "name": "ssoSettingsApi", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-11-08T09:50:01Z" }, "spec": { @@ -2142,8 +2130,8 @@ { "metadata": { "name": "ssoSettingsLDAP", - "resourceVersion": "1718624569822", - "creationTimestamp": "2024-06-17T11:42:49Z" + "resourceVersion": "1718727528075", + "creationTimestamp": "2024-06-18T16:18:48Z" }, "spec": { "description": "Use the new SSO Settings API to configure LDAP", @@ -2156,7 +2144,7 @@ { "metadata": { "name": "ssoSettingsSAML", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-03-14T11:04:45Z" }, "spec": { @@ -2169,7 +2157,7 @@ { "metadata": { "name": "storage", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-03-17T17:19:23Z" }, "spec": { @@ -2181,7 +2169,7 @@ { "metadata": { "name": "tableSharedCrosshair", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-12-13T09:33:14Z" }, "spec": { @@ -2194,7 +2182,7 @@ { "metadata": { "name": "teamHttpHeaders", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-10-17T10:23:54Z" }, "spec": { @@ -2206,7 +2194,7 @@ { "metadata": { "name": "tlsMemcached", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2024-05-09T19:12:08Z" }, "spec": { @@ -2219,7 +2207,7 @@ { "metadata": { "name": "topnav", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2022-06-20T14:25:43Z" }, "spec": { @@ -2231,7 +2219,7 @@ { "metadata": { "name": "traceQLStreaming", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-26T13:33:16Z" }, "spec": { @@ -2244,7 +2232,7 @@ { "metadata": { "name": "transformationsRedesign", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-07-12T16:35:49Z" }, "spec": { @@ -2258,11 +2246,8 @@ { "metadata": { "name": "transformationsVariableSupport", - "resourceVersion": "1718190346540", - "creationTimestamp": "2023-10-04T14:28:46Z", - "annotations": { - "grafana.app/updatedTimestamp": "2024-06-12 11:05:46.540869 +0000 UTC" - } + "resourceVersion": "1718727528075", + "creationTimestamp": "2023-10-04T14:28:46Z" }, "spec": { "description": "Allows using variables in transformations", @@ -2274,7 +2259,7 @@ { "metadata": { "name": "unifiedRequestLog", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-03-31T13:38:09Z" }, "spec": { @@ -2286,7 +2271,7 @@ { "metadata": { "name": "unifiedStorage", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-12-06T20:21:21Z" }, "spec": { @@ -2299,7 +2284,7 @@ { "metadata": { "name": "vizAndWidgetSplit", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-06-27T10:22:13Z" }, "spec": { @@ -2312,7 +2297,7 @@ { "metadata": { "name": "wargamesTesting", - "resourceVersion": "1717578796182", + "resourceVersion": "1718727528075", "creationTimestamp": "2023-09-13T18:32:01Z" }, "spec": { From 7c69f3657b5db4f6168af3d2b6e6796b7bb568d7 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Tue, 18 Jun 2024 16:02:33 -0500 Subject: [PATCH 008/122] Notifications: Redact URL from errors (#85687) * Remove url logs and redact * Reinclude redacted URL --- pkg/services/notifications/webhook.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index 6ff7df5efe8..7fbaf59eaad 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -4,10 +4,12 @@ import ( "bytes" "context" "crypto/tls" + "errors" "fmt" "io" "net" "net/http" + "net/url" "time" "github.com/grafana/grafana/pkg/util" @@ -62,6 +64,11 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook * if err != nil { return err } + url, err := url.Parse(webhook.Url) + if err != nil { + // Should not be possible - NewRequestWithContext should also err if the URL is bad. + return err + } if webhook.ContentType == "" { webhook.ContentType = "application/json" @@ -80,7 +87,7 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook * resp, err := netClient.Do(request) if err != nil { - return err + return redactURL(err) } defer func() { if err := resp.Body.Close(); err != nil { @@ -96,16 +103,25 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook * if webhook.Validation != nil { err := webhook.Validation(body, resp.StatusCode) if err != nil { - ns.log.Debug("Webhook failed validation", "url", webhook.Url, "statuscode", resp.Status, "body", string(body)) + ns.log.Debug("Webhook failed validation", "url", url.Redacted(), "statuscode", resp.Status, "body", string(body), "error", err) return fmt.Errorf("webhook failed validation: %w", err) } } if resp.StatusCode/100 == 2 { - ns.log.Debug("Webhook succeeded", "url", webhook.Url, "statuscode", resp.Status) + ns.log.Debug("Webhook succeeded", "url", url.Redacted(), "statuscode", resp.Status) return nil } - ns.log.Debug("Webhook failed", "url", webhook.Url, "statuscode", resp.Status, "body", string(body)) + ns.log.Debug("Webhook failed", "url", url.Redacted(), "statuscode", resp.Status, "body", string(body)) return fmt.Errorf("webhook response status %v", resp.Status) } + +func redactURL(err error) error { + var e *url.Error + if !errors.As(err, &e) { + return err + } + e.URL = "" + return e +} From 74bcfb284c16f99518d48d61c09e563b28ef6502 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 18 Jun 2024 21:51:20 -0500 Subject: [PATCH 009/122] Heatmap: Use dashboard timeRange for auto-sizing x buckets (#89382) --- .../transformers/calculateHeatmap/heatmap.ts | 26 +++++++++++---- .../plugins/panel/heatmap/HeatmapPanel.tsx | 12 +++++-- public/app/plugins/panel/heatmap/fields.ts | 32 +++++++++++++------ public/app/plugins/panel/heatmap/module.tsx | 7 +++- .../app/plugins/panel/heatmap/suggestions.ts | 7 +++- 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 1de1d638361..538b2c9a97a 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -15,6 +15,7 @@ import { durationToMilliseconds, parseDuration, TransformationApplicabilityLevels, + TimeRange, } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { config } from '@grafana/runtime'; @@ -292,7 +293,14 @@ export function prepBucketFrames(frames: DataFrame[]): DataFrame[] { })); } -export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCalculationOptions): DataFrame { +interface HeatmapCalculationOptionsWithTimeRange extends HeatmapCalculationOptions { + timeRange?: TimeRange; +} + +export function calculateHeatmapFromData( + frames: DataFrame[], + options: HeatmapCalculationOptionsWithTimeRange +): DataFrame { // Find fields in the heatmap const { xField, yField, xs, ys } = findHeatmapFields(frames); @@ -329,6 +337,9 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined, yLog: scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as 2 | 10 | undefined) : undefined, + + xMin: options.timeRange?.from.valueOf(), + xMax: options.timeRange?.to.valueOf(), }); const frame = { @@ -460,20 +471,23 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { let ySorted = opts?.ySorted ?? false; // find x and y limits to pre-compute buckets struct - let minX = xSorted ? xs[0] : Infinity; + let minX = opts?.xMin ?? (xSorted ? xs[0] : Infinity); let minY = ySorted ? ys[0] : Infinity; - let maxX = xSorted ? xs[len - 1] : -Infinity; + let maxX = opts?.xMax ?? (xSorted ? xs[len - 1] : -Infinity); let maxY = ySorted ? ys[len - 1] : -Infinity; let yExp = opts?.yLog; + let withPredefX = opts?.xMin != null && opts?.xMax != null; + let withPredefY = opts?.yMin != null && opts?.yMax != null; + for (let i = 0; i < len; i++) { - if (!xSorted) { + if (!xSorted && !withPredefX) { minX = Math.min(minX, xs[i]); maxX = Math.max(maxX, xs[i]); } - if (!ySorted) { + if (!ySorted && !withPredefY) { if (!yExp || ys[i] > 0) { minY = Math.min(minY, ys[i]); maxY = Math.max(maxY, ys[i]); @@ -500,7 +514,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { } if (xMode === HeatmapCalculationMode.Count) { - // TODO: optionally use view range min/max instead of data range for bucket sizing let approx = (maxX - minX) / Math.max(xBinIncr - 1, 1); // nice-ify let xIncrs = opts?.xTime ? niceTimeIncrs : niceLinearIncrs; @@ -509,7 +522,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { } if (yMode === HeatmapCalculationMode.Count) { - // TODO: optionally use view range min/max instead of data range for bucket sizing let approx = (maxY - minY) / Math.max(yBinIncr - 1, 1); // nice-ify let yIncrs = opts?.yTime ? niceTimeIncrs : niceLinearIncrs; diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index f3cde37e399..b7ce5ab2be9 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -59,11 +59,19 @@ export const HeatmapPanel = ({ const info = useMemo(() => { try { - return prepareHeatmapData(data.series, data.annotations, options, palette, theme, replaceVariables); + return prepareHeatmapData({ + frames: data.series, + annotations: data.annotations, + options, + palette, + theme, + replaceVariables, + timeRange, + }); } catch (ex) { return { warning: `${ex}` }; } - }, [data.series, data.annotations, options, palette, theme, replaceVariables]); + }, [data.series, data.annotations, options, palette, theme, replaceVariables, timeRange]); const facets = useMemo(() => { let exemplarsXFacet: number[] | undefined = []; // "Time" field diff --git a/public/app/plugins/panel/heatmap/fields.ts b/public/app/plugins/panel/heatmap/fields.ts index aeb79a3538f..534d963e92e 100644 --- a/public/app/plugins/panel/heatmap/fields.ts +++ b/public/app/plugins/panel/heatmap/fields.ts @@ -10,6 +10,7 @@ import { GrafanaTheme2, InterpolateFunction, outerJoinDataFrames, + TimeRange, ValueFormatter, } from '@grafana/data'; import { parseSampleValue, sortSeriesByLabel } from '@grafana/prometheus'; @@ -65,14 +66,25 @@ export interface HeatmapData { warning?: string; } -export function prepareHeatmapData( - frames: DataFrame[], - annotations: DataFrame[] | undefined, - options: Options, - palette: string[], - theme: GrafanaTheme2, - replaceVariables: InterpolateFunction = (v) => v -): HeatmapData { +interface PrepareHeatmapDataOptions { + frames: DataFrame[]; + annotations?: DataFrame[]; + options: Options; + palette: string[]; + theme: GrafanaTheme2; + replaceVariables?: InterpolateFunction; + timeRange?: TimeRange; +} + +export function prepareHeatmapData({ + frames, + annotations, + options, + palette, + theme, + replaceVariables = (v) => v, + timeRange, +}: PrepareHeatmapDataOptions): HeatmapData { if (!frames?.length) { return {}; } @@ -104,7 +116,7 @@ export function prepareHeatmapData( } return getDenseHeatmapData( - calculateHeatmapFromData(frames, optionsCopy.calculation ?? {}), + calculateHeatmapFromData(frames, { ...options.calculation, timeRange }), exemplars, optionsCopy, palette, @@ -113,7 +125,7 @@ export function prepareHeatmapData( } return getDenseHeatmapData( - calculateHeatmapFromData(frames, options.calculation ?? {}), + calculateHeatmapFromData(frames, { ...options.calculation, timeRange }), exemplars, options, palette, diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 8c35609950a..660e71e7261 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -53,7 +53,12 @@ export const plugin = new PanelPlugin(HeatmapPanel) // NOTE: this feels like overkill/expensive just to assert if we have an ordinal y // can probably simplify without doing full dataprep const palette = quantizeScheme(opts.color, config.theme2); - const v = prepareHeatmapData(context.data, undefined, opts, palette, config.theme2); + const v = prepareHeatmapData({ + frames: context.data, + options: opts, + palette, + theme: config.theme2, + }); isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null; } catch {} } diff --git a/public/app/plugins/panel/heatmap/suggestions.ts b/public/app/plugins/panel/heatmap/suggestions.ts index e9778229e7e..f49de761e85 100644 --- a/public/app/plugins/panel/heatmap/suggestions.ts +++ b/public/app/plugins/panel/heatmap/suggestions.ts @@ -20,7 +20,12 @@ export class HeatmapSuggestionsSupplier { } const palette = quantizeScheme(defaultOptions.color, config.theme2); - const info = prepareHeatmapData(builder.data.series, undefined, defaultOptions, palette, config.theme2); + const info = prepareHeatmapData({ + frames: builder.data.series, + options: defaultOptions, + palette, + theme: config.theme2, + }); if (!info || info.warning) { return; } From a5e02f033a878999c4134aaca71c1975048cbf8b Mon Sep 17 00:00:00 2001 From: Yuri Kotov Date: Wed, 19 Jun 2024 13:19:27 +0700 Subject: [PATCH 010/122] Prometheus: Fix query builder visualization when a query has by() clause for quantile (#88480) Prometheus query builder: add support for by() clause for quantile aggregation --- .../grafana-prometheus/src/querybuilder/aggregations.ts | 4 ++++ .../src/querybuilder/components/promQail/state/templates.ts | 4 ++++ packages/grafana-prometheus/src/querybuilder/operations.ts | 6 ------ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/grafana-prometheus/src/querybuilder/aggregations.ts b/packages/grafana-prometheus/src/querybuilder/aggregations.ts index d4129c3665e..8fff6505173 100644 --- a/packages/grafana-prometheus/src/querybuilder/aggregations.ts +++ b/packages/grafana-prometheus/src/querybuilder/aggregations.ts @@ -28,6 +28,10 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] { params: [{ name: 'Identifier', type: 'string' }], defaultParams: ['count'], }), + ...createAggregationOperationWithParam(PromOperationId.Quantile, { + params: [{ name: 'Value', type: 'number' }], + defaultParams: [1], + }), createAggregationOverTime(PromOperationId.SumOverTime), createAggregationOverTime(PromOperationId.AvgOverTime), createAggregationOverTime(PromOperationId.MinOverTime), diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/state/templates.ts b/packages/grafana-prometheus/src/querybuilder/components/promQail/state/templates.ts index 330de01da2f..6e455accdb9 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/state/templates.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/promQail/state/templates.ts @@ -76,6 +76,10 @@ export const generalTemplates: TemplateData[] = [ template: 'count_values("aaaa",metric_a{})', description: 'Count number of label values for a label named "aaaa"', }, + { + template: 'quantile by(l) (1,metric_a)', + description: 'Quantile of series in the metric "metric_a" grouped by the label "l"', + }, ]; export const counterTemplates: TemplateData[] = [ diff --git a/packages/grafana-prometheus/src/querybuilder/operations.ts b/packages/grafana-prometheus/src/querybuilder/operations.ts index cb83b0a44d6..afd77331239 100644 --- a/packages/grafana-prometheus/src/querybuilder/operations.ts +++ b/packages/grafana-prometheus/src/querybuilder/operations.ts @@ -230,12 +230,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { id: PromOperationId.Pi, renderer: (model) => `${model.id}()`, }), - createFunction({ - id: PromOperationId.Quantile, - params: [{ name: 'Value', type: 'number' }], - defaultParams: [1], - renderer: functionRendererLeft, - }), createFunction({ id: PromOperationId.Rad }), createRangeFunction(PromOperationId.Resets), createFunction({ From 76047d9365114714b4da7ff0a687aec0aadb9d41 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 19 Jun 2024 08:19:49 +0200 Subject: [PATCH 011/122] Prometheus: Don't use match[] parameter if there is no metric (#89352) don't use match[] parameter if there is no metric --- .../src/language_provider.test.ts | 14 ++++++++++++++ .../grafana-prometheus/src/language_provider.ts | 12 +++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index dd7dec7ce71..e1496bd06d8 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -505,6 +505,20 @@ describe('Language completion provider', () => { ) ); }); + + it('should dont send match[] parameter if there is no metric', async () => { + const mockQueries: PromQuery[] = [ + { + refId: 'A', + expr: '', + }, + ]; + const fetchLabel = languageProvider.fetchLabels; + const requestSpy = jest.spyOn(languageProvider, 'request'); + await fetchLabel(tr, mockQueries); + expect(requestSpy).toHaveBeenCalled(); + expect(requestSpy.mock.calls[0][0].indexOf('match[]')).toEqual(-1); + }); }); }); diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index a6105c988a3..b7ec09163e9 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -219,11 +219,13 @@ export default class PromQlLanguageProvider extends LanguageProvider { const searchParams = new URLSearchParams({ ...timeParams }); queries?.forEach((q) => { const visualQuery = buildVisualQueryFromString(q.expr); - searchParams.append('match[]', visualQuery.query.metric); - if (visualQuery.query.binaryQueries) { - visualQuery.query.binaryQueries.forEach((bq) => { - searchParams.append('match[]', bq.query.metric); - }); + if (visualQuery.query.metric !== '') { + searchParams.append('match[]', visualQuery.query.metric); + if (visualQuery.query.binaryQueries) { + visualQuery.query.binaryQueries.forEach((bq) => { + searchParams.append('match[]', bq.query.metric); + }); + } } }); From dfcda2b14a298a7290d9cf138eb224e1fb43bdb0 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 19 Jun 2024 08:22:16 +0200 Subject: [PATCH 012/122] InfluxDB: Don't interpolate bucket keyword in flux language if it is part of a join query (#89298) don't interpolate bucket keyword if it is part of a join query --- pkg/tsdb/influxdb/flux/macros.go | 29 +++++++++++++++++++-------- pkg/tsdb/influxdb/flux/macros_test.go | 5 +++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pkg/tsdb/influxdb/flux/macros.go b/pkg/tsdb/influxdb/flux/macros.go index 4dce9d76822..fbec2b4d1fe 100644 --- a/pkg/tsdb/influxdb/flux/macros.go +++ b/pkg/tsdb/influxdb/flux/macros.go @@ -25,27 +25,40 @@ func interpolateInterval(flux string, interval time.Duration) string { var fluxVariableFilterExp = regexp.MustCompile(`(?m)([a-zA-Z]+)\.([a-zA-Z]+)`) func interpolateFluxSpecificVariables(query queryModel) string { + rawQuery := query.RawQuery flux := query.RawQuery - matches := fluxVariableFilterExp.FindAllStringSubmatch(flux, -1) + matches := fluxVariableFilterExp.FindAllStringSubmatchIndex(rawQuery, -1) if matches != nil { timeRange := query.TimeRange from := timeRange.From.UTC().Format(time.RFC3339Nano) to := timeRange.To.UTC().Format(time.RFC3339Nano) for _, match := range matches { - switch match[2] { + // For query "range(start: v.timeRangeStart, stop: v.timeRangeStop)" + // rawQuery[match[0]:match[1]] will be v.timeRangeStart + // rawQuery[match[2]:match[3]] will be v + // rawQuery[match[4]:match[5]] will be timeRangeStart + fullMatch := rawQuery[match[0]:match[1]] + key := rawQuery[match[4]:match[5]] + + switch key { case "timeRangeStart": - flux = strings.ReplaceAll(flux, match[0], from) + flux = strings.ReplaceAll(flux, fullMatch, from) case "timeRangeStop": - flux = strings.ReplaceAll(flux, match[0], to) + flux = strings.ReplaceAll(flux, fullMatch, to) case "windowPeriod": - flux = strings.ReplaceAll(flux, match[0], query.Interval.String()) + flux = strings.ReplaceAll(flux, fullMatch, query.Interval.String()) case "bucket": - flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.Bucket+"\"") + // Check if 'bucket' is part of a join query + beforeMatch := rawQuery[:match[0]] + if strings.Contains(beforeMatch, "join.") { + continue + } + flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.Bucket+"\"") case "defaultBucket": - flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.DefaultBucket+"\"") + flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.DefaultBucket+"\"") case "organization": - flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.Organization+"\"") + flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.Organization+"\"") } } } diff --git a/pkg/tsdb/influxdb/flux/macros_test.go b/pkg/tsdb/influxdb/flux/macros_test.go index 9bf0fa3570a..283835ab24e 100644 --- a/pkg/tsdb/influxdb/flux/macros_test.go +++ b/pkg/tsdb/influxdb/flux/macros_test.go @@ -31,6 +31,11 @@ func TestInterpolate(t *testing.T) { before: `v.timeRangeStart, something.timeRangeStop, XYZ.bucket, uuUUu.defaultBucket, aBcDefG.organization, window.windowPeriod, a91{}.bucket, $__interval, $__interval_ms`, after: `2021-09-22T10:12:51.310985041Z, 2021-09-22T11:12:51.310985042Z, "grafana2", "grafana3", "grafana1", 1m1.258s, a91{}.bucket, 1m, 61258`, }, + { + name: "don't interpolate bucket variable in join query", + before: `range(start: v.timeRangeStart, stop: v.timeRangeStop) join.left(left: left |> group(), right: right,on:((l,r) => l.bucket == r.id), as: ((l, r) => ({l with name: r.name})))`, + after: `range(start: 2021-09-22T10:12:51.310985041Z, stop: 2021-09-22T11:12:51.310985042Z) join.left(left: left |> group(), right: right,on:((l,r) => l.bucket == r.id), as: ((l, r) => ({l with name: r.name})))`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From e5a50a7db80a55d22af4f0a97acdaa1aa03adc18 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 19 Jun 2024 08:23:21 +0200 Subject: [PATCH 013/122] InfluxDB: Fix sending range info on variable editor (#89347) send range info in range field --- go.work.sum | 13 +++++++++++-- public/app/plugins/datasource/influxdb/variables.ts | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/go.work.sum b/go.work.sum index 917fda449c8..d8edfcf36bd 100644 --- a/go.work.sum +++ b/go.work.sum @@ -423,6 +423,7 @@ github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/KimMachineGun/automemlimit v0.6.0 h1:p/BXkH+K40Hax+PuWWPQ478hPjsp9h1CPDhLlA3Z37E= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -696,6 +697,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= @@ -855,8 +857,6 @@ github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJ github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd h1:hSkbZ9XSyjyBirMeqSqUrK+9HboWrweVlzRNqoBi2d4= github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= @@ -915,6 +915,7 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ= github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= @@ -1021,6 +1022,7 @@ github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52Cu github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLwFEN7T+Flr8sw= github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao= github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o= @@ -1147,12 +1149,15 @@ github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+v github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo= github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= +github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E= github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8= github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= +github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY= github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/oapi-codegen/testutil v1.0.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw= github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= @@ -1232,6 +1237,7 @@ github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAy github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -1254,6 +1260,7 @@ github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= @@ -1294,6 +1301,7 @@ github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8= +github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk= github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= @@ -1362,6 +1370,7 @@ github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvF github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= diff --git a/public/app/plugins/datasource/influxdb/variables.ts b/public/app/plugins/datasource/influxdb/variables.ts index a51430f62bd..f3ee7eb75b5 100644 --- a/public/app/plugins/datasource/influxdb/variables.ts +++ b/public/app/plugins/datasource/influxdb/variables.ts @@ -40,7 +40,7 @@ export class InfluxVariableSupport extends CustomVariableSupport ({ data: results }))); From 51c858f32e5e8308b4d2af8abf79d24bbb8f11c2 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:31:01 +0300 Subject: [PATCH 014/122] Fix dashboard crash on dashboard leave in panel edit (#89353) * Fix dashboard crashing when leaving dashboard in panel edit * add test * test --- .../dashboard-scene/scene/DashboardScene.test.tsx | 14 ++++++++++++++ .../dashboard-scene/scene/DashboardScene.tsx | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index bb4488f705f..91e50fba9e1 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -200,6 +200,20 @@ describe('DashboardScene', () => { expect(resoredLayout.state.children.map((c) => c.state.key)).toEqual(originalPanelOrder); }); + it('Should exit edit mode and discard panel changes if leaving the dashboard while in panel edit', () => { + const panel = findVizPanelByKey(scene, 'panel-1'); + const editPanel = buildPanelEditScene(panel!); + scene.setState({ + editPanel, + }); + + expect(scene.state.editPanel!['_discardChanges']).toBe(false); + + scene.exitEditMode({ skipConfirm: true }); + + expect(scene.state.editPanel!['_discardChanges']).toBe(true); + }); + it.each` prop | value ${'title'} | ${'new title'} diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 83b0edbc9c3..e2bc1828f39 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -317,6 +317,13 @@ export class DashboardScene extends SceneObjectBase { this.setState({ isEditing: false }); } + // if we are in edit panel, we need to onDiscard() + // so the useEffect cleanup comes later and + // doesn't try to commit the changes + if (this.state.editPanel) { + this.state.editPanel.onDiscard(); + } + // Disable grid dragging this.propagateEditModeChange(); } From a9736cf7dcdd0021d703d5861a8a2e5bf30960c3 Mon Sep 17 00:00:00 2001 From: Nihal <38865967+wasim-nihal@users.noreply.github.com> Date: Wed, 19 Jun 2024 12:09:53 +0530 Subject: [PATCH 015/122] InfluxDB: Fix query builder produces invalid SQL query when using wildcard column name (#89032) * influx: changes to not wrap * with double quotes for column name Signed-off-by: Syed Nihal * added unit test for https://github.com/grafana/grafana/issues/88008 Signed-off-by: Syed Nihal --------- Signed-off-by: Syed Nihal --- .../datasource/influxdb/fsql/sqlUtil.test.ts | 25 +++++++++++++++++++ .../datasource/influxdb/fsql/sqlUtil.ts | 15 ++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts index 4864004b8ec..7ca87d9f8ae 100644 --- a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts +++ b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.test.ts @@ -78,4 +78,29 @@ describe('toRawSql', () => { const result = toRawSql(testQuery); expect(result).toEqual(expected); }); + + it('should not wrap * with quote', () => { + const expected = 'SELECT * FROM "TestValue" WHERE "time" >= $__timeFrom AND "time" <= $__timeTo LIMIT 50'; + const testQuery: SQLQuery = { + refId: 'A', + sql: { + limit: 50, + columns: [ + { + parameters: [ + { + name: '*', + type: QueryEditorExpressionType.FunctionParameter, + }, + ], + type: QueryEditorExpressionType.Function, + }, + ], + }, + dataset: 'iox', + table: 'TestValue', + }; + const result = toRawSql(testQuery); + expect(result).toEqual(expected); + }); }); diff --git a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts index 41317454a95..8d357fce734 100644 --- a/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts +++ b/public/app/plugins/datasource/influxdb/fsql/sqlUtil.ts @@ -26,7 +26,10 @@ export function toRawSql({ sql, table }: SQLQuery): string { } // wrapping the column name with quotes - const sc = sql.columns.map((c) => ({ ...c, parameters: c.parameters?.map((p) => ({ ...p, name: `"${p.name}"` })) })); + const sc = sql.columns.map((c) => ({ + ...c, + parameters: c.parameters?.map((p) => ({ ...p, name: formatTableName(p.name) })), + })); rawQuery += createSelectClause(sc); if (table) { @@ -66,6 +69,16 @@ export function toRawSql({ sql, table }: SQLQuery): string { return rawQuery; } +// When the column name is *, do not wrap the column name in double-quotes. +// See: https://github.com/grafana/grafana/issues/88008 +function formatTableName(parameter: string | undefined): string { + if (parameter === '*') { + return parameter; + } + + return `"${parameter}"`; +} + const isLimit = (limit: number | undefined): boolean => limit !== undefined && limit >= 0; // Puts double quotes (") around the identifier if it is necessary. From b43411631b616c962d86174134c60961bd0a98ee Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 19 Jun 2024 10:00:49 +0100 Subject: [PATCH 016/122] Echo: Don't notify if PerformanceBackend errors when flushing (#89379) --- .../services/echo/backends/PerformanceBackend.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/core/services/echo/backends/PerformanceBackend.ts b/public/app/core/services/echo/backends/PerformanceBackend.ts index 29ff37882bc..7fd3a18d572 100644 --- a/public/app/core/services/echo/backends/PerformanceBackend.ts +++ b/public/app/core/services/echo/backends/PerformanceBackend.ts @@ -32,9 +32,17 @@ export class PerformanceBackend implements EchoBackend { + // Just swallow this error - it's non-critical + }); this.buffer = []; }; From db78604bd4942b8508a994b3496410cdb6f05aa7 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 19 Jun 2024 10:37:20 +0100 Subject: [PATCH 017/122] I18n: Add utility for locale-aware duration formatting (#89351) * I18n: Add utility for locale-aware duration formatting * fix message * add fallback --- package.json | 1 + public/app/core/internationalization/dates.ts | 20 ++++++++++++ .../app/core/internationalization/index.tsx | 25 ++++++++++----- .../app/core/internationalization/types.d.ts | 13 ++++++++ public/app/features/admin/UserSessions.tsx | 4 +-- public/app/features/profile/UserSessions.tsx | 5 +-- yarn.lock | 31 +++++++++++++++++++ 7 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 public/app/core/internationalization/dates.ts create mode 100644 public/app/core/internationalization/types.d.ts diff --git a/package.json b/package.json index 9f32bc39ac5..1968f5ffd78 100644 --- a/package.json +++ b/package.json @@ -244,6 +244,7 @@ "@emotion/react": "11.11.4", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.16", + "@formatjs/intl-durationformat": "^0.2.4", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/aws-sdk": "0.3.3", "@grafana/azure-sdk": "0.0.3", diff --git a/public/app/core/internationalization/dates.ts b/public/app/core/internationalization/dates.ts new file mode 100644 index 00000000000..429db533901 --- /dev/null +++ b/public/app/core/internationalization/dates.ts @@ -0,0 +1,20 @@ +import '@formatjs/intl-durationformat/polyfill'; + +import { getI18next } from './index'; + +export function formatDate(value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string { + if (typeof value === 'string') { + return formatDate(new Date(value), format); + } + + const i18n = getI18next(); + const dateFormatter = new Intl.DateTimeFormat(i18n.language, format); + return dateFormatter.format(value); +} + +export function formatDuration(duration: Intl.DurationInput, options: Intl.DurationFormatOptions = {}) { + const i18n = getI18next(); + + const dateFormatter = new Intl.DurationFormat(i18n.language, options); + return dateFormatter.format(duration); +} diff --git a/public/app/core/internationalization/index.tsx b/public/app/core/internationalization/index.tsx index 812fc4f83b7..f111be88b98 100644 --- a/public/app/core/internationalization/index.tsx +++ b/public/app/core/internationalization/index.tsx @@ -7,6 +7,7 @@ import { DEFAULT_LANGUAGE, NAMESPACES, VALID_LANGUAGES } from './constants'; import { loadTranslations } from './loadTranslations'; let tFunc: TFunction | undefined; +let i18nInstance: typeof i18n; export async function initializeI18n(language: string): Promise<{ language: string | undefined }> { // This is a placeholder so we can put a 'comment' in the message json files. @@ -28,7 +29,7 @@ export async function initializeI18n(language: string): Promise<{ language: stri ns: NAMESPACES, }; - let i18nInstance = i18n; + i18nInstance = i18n; if (language === 'detect') { i18nInstance = i18nInstance.use(LanguageDetector); const detection: DetectorOptions = { order: ['navigator'], caches: [] }; @@ -79,10 +80,20 @@ export const t = (id: string, defaultMessage: string, values?: Record { - if (typeof value === 'string') { - return i18nDate(new Date(value), format); +export function getI18next() { + if (!tFunc) { + if (process.env.NODE_ENV !== 'test') { + console.warn( + 'An attempt to internationalize was made before it was initialized. This was probably caused by calling a locale-aware function in the root module scope, instead of in render' + ); + } + + if (process.env.NODE_ENV === 'development') { + throw new Error('getI18next was called before i18n was initialized'); + } + + return i18n; } - const dateFormatter = new Intl.DateTimeFormat(i18n.language, format); - return dateFormatter.format(value); -}; + + return i18nInstance || i18n; +} diff --git a/public/app/core/internationalization/types.d.ts b/public/app/core/internationalization/types.d.ts new file mode 100644 index 00000000000..7a4ac64930c --- /dev/null +++ b/public/app/core/internationalization/types.d.ts @@ -0,0 +1,13 @@ +import { + DurationFormatConstructor, + DurationFormatOptions as _DurationFormatOptions, + DurationInput as _DurationInput, +} from '@formatjs/intl-durationformat/src/types'; + +declare global { + namespace Intl { + const DurationFormat: DurationFormatConstructor; + type DurationFormatOptions = _DurationFormatOptions; + type DurationInput = _DurationInput; + } +} diff --git a/public/app/features/admin/UserSessions.tsx b/public/app/features/admin/UserSessions.tsx index 465072ba34d..91f8694998b 100644 --- a/public/app/features/admin/UserSessions.tsx +++ b/public/app/features/admin/UserSessions.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; -import { i18nDate } from 'app/core/internationalization'; +import { formatDate } from 'app/core/internationalization/dates'; import { AccessControlAction, UserSession } from 'app/types'; interface Props { @@ -68,7 +68,7 @@ class BaseUserSessions extends PureComponent { sessions.map((session, index) => ( {session.isActive ? 'Now' : session.seenAt} - {i18nDate(session.createdAt, { dateStyle: 'long' })} + {formatDate(session.createdAt, { dateStyle: 'long' })} {session.clientIp} {`${session.browser} on ${session.os} ${session.osVersion}`} diff --git a/public/app/features/profile/UserSessions.tsx b/public/app/features/profile/UserSessions.tsx index ec9dbb034e7..9d93c5a9e08 100644 --- a/public/app/features/profile/UserSessions.tsx +++ b/public/app/features/profile/UserSessions.tsx @@ -4,7 +4,8 @@ import React, { PureComponent } from 'react'; import { selectors } from '@grafana/e2e-selectors'; import { Button, Icon, LoadingPlaceholder } from '@grafana/ui'; -import { i18nDate, Trans } from 'app/core/internationalization'; +import { Trans } from 'app/core/internationalization'; +import { formatDate } from 'app/core/internationalization/dates'; import { UserSession } from 'app/types'; interface Props { @@ -50,7 +51,7 @@ class UserSessions extends PureComponent { {sessions.map((session: UserSession, index) => ( {session.isActive ? Now : {session.seenAt}} - {i18nDate(session.createdAt, { dateStyle: 'long' })} + {formatDate(session.createdAt, { dateStyle: 'long' })} {session.clientIp} {session.browser} on {session.os} {session.osVersion} diff --git a/yarn.lock b/yarn.lock index 4529834161a..079e080a949 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2503,6 +2503,16 @@ __metadata: languageName: node linkType: hard +"@formatjs/ecma402-abstract@npm:2.0.0": + version: 2.0.0 + resolution: "@formatjs/ecma402-abstract@npm:2.0.0" + dependencies: + "@formatjs/intl-localematcher": "npm:0.5.4" + tslib: "npm:^2.4.0" + checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234 + languageName: node + linkType: hard + "@formatjs/fast-memoize@npm:1.2.6": version: 1.2.6 resolution: "@formatjs/fast-memoize@npm:1.2.6" @@ -2533,6 +2543,17 @@ __metadata: languageName: node linkType: hard +"@formatjs/intl-durationformat@npm:^0.2.4": + version: 0.2.4 + resolution: "@formatjs/intl-durationformat@npm:0.2.4" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.0.0" + "@formatjs/intl-localematcher": "npm:0.5.4" + tslib: "npm:^2.4.0" + checksum: 10/5f500409a20d18967e17ffbc222f9b4c4bf7ef08cce20023c33f06d1989c2bc4cf700d1dd1d048748d0a36c882109d5375896a4964d6700f73ec18914c6de4ba + languageName: node + linkType: hard + "@formatjs/intl-localematcher@npm:0.2.31": version: 0.2.31 resolution: "@formatjs/intl-localematcher@npm:0.2.31" @@ -2542,6 +2563,15 @@ __metadata: languageName: node linkType: hard +"@formatjs/intl-localematcher@npm:0.5.4": + version: 0.5.4 + resolution: "@formatjs/intl-localematcher@npm:0.5.4" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/780cb29b42e1ea87f2eb5db268577fcdc53da52d9f096871f3a1bb78603b4ba81d208ea0b0b9bc21548797c941ce435321f62d2522795b83b740f90b0ceb5778 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -17036,6 +17066,7 @@ __metadata: "@emotion/react": "npm:11.11.4" "@fingerprintjs/fingerprintjs": "npm:^3.4.2" "@floating-ui/react": "npm:0.26.16" + "@formatjs/intl-durationformat": "npm:^0.2.4" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/aws-sdk": "npm:0.3.3" "@grafana/azure-sdk": "npm:0.0.3" From 8eabef1f91b48dcf50dab3ecc4304cea3c901acc Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Wed, 19 Jun 2024 11:40:22 +0200 Subject: [PATCH 018/122] Alerting: Update remote alertmanager to use extended receivers API. (#89253) * Alerting: Update remote alertmanager to use extended receivers API. * Update integration test and Mimir image * Update Mimir image in more places --- .drone.yml | 12 ++++++------ .../blocks/mimir_backend/docker-compose.yaml | 2 +- pkg/services/ngalert/remote/alertmanager.go | 17 +---------------- .../ngalert/remote/alertmanager_test.go | 8 +++++++- .../remote/client/alertmanager_configuration.go | 16 +++++++++++++++- pkg/services/ngalert/remote/client/mimir.go | 3 +++ scripts/drone/utils/images.star | 2 +- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/.drone.yml b/.drone.yml index 658e2d4adde..2efa39792df 100644 --- a/.drone.yml +++ b/.drone.yml @@ -865,7 +865,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP + image: grafana/mimir-alpine:r295-a23e559 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -1312,7 +1312,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP + image: grafana/mimir-alpine:r295-a23e559 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -2299,7 +2299,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP + image: grafana/mimir-alpine:r295-a23e559 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4154,7 +4154,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP + image: grafana/mimir-alpine:r295-a23e559 name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4673,7 +4673,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r295-a23e559 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine @@ -4708,7 +4708,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack - trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8 - trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine - - trivy --exit-code 1 --severity HIGH,CRITICAL us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP + - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r295-a23e559 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32 - trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine diff --git a/devenv/docker/blocks/mimir_backend/docker-compose.yaml b/devenv/docker/blocks/mimir_backend/docker-compose.yaml index 76a19755d9f..2297d16acc6 100644 --- a/devenv/docker/blocks/mimir_backend/docker-compose.yaml +++ b/devenv/docker/blocks/mimir_backend/docker-compose.yaml @@ -1,5 +1,5 @@ mimir_backend: - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP + image: grafana/mimir-alpine:r295-a23e559 container_name: mimir_backend command: - -target=backend diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 4feca91ed4b..2014220eda9 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -15,7 +15,6 @@ import ( amalert "github.com/prometheus/alertmanager/api/v2/client/alert" amalertgroup "github.com/prometheus/alertmanager/api/v2/client/alertgroup" amgeneral "github.com/prometheus/alertmanager/api/v2/client/general" - amreceiver "github.com/prometheus/alertmanager/api/v2/client/receiver" amsilence "github.com/prometheus/alertmanager/api/v2/client/silence" "github.com/prometheus/client_golang/prometheus" @@ -487,21 +486,7 @@ func (am *Alertmanager) GetStatus(ctx context.Context) (apimodels.GettableStatus } func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) { - params := amreceiver.NewGetReceiversParamsWithContext(ctx) - res, err := am.amClient.Receiver.GetReceivers(params) - if err != nil { - return []apimodels.Receiver{}, err - } - - rcvs := make([]apimodels.Receiver, len(res.Payload)) - for i, rcv := range res.Payload { - rcvs[i] = apimodels.Receiver{ - Name: *rcv.Name, - Integrations: []apimodels.Integration{}, - } - } - - return rcvs, nil + return am.mimirClient.GetReceivers(ctx) } func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*notifier.TestReceiversResult, error) { diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 5016ab49e38..9617a0f820d 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -717,7 +717,13 @@ func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) { // We should start with the default config. rcvs, err := am.GetReceivers(context.Background()) require.NoError(t, err) - require.Equal(t, "empty-receiver", rcvs[0].Name) + require.Equal(t, []apimodels.Receiver{ + { + Active: true, + Name: "empty-receiver", + Integrations: []apimodels.Integration{}, + }, + }, rcvs) } func genAlert(active bool, labels map[string]string) amv2.PostableAlert { diff --git a/pkg/services/ngalert/remote/client/alertmanager_configuration.go b/pkg/services/ngalert/remote/client/alertmanager_configuration.go index f3a66451163..ebb61ce9196 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_configuration.go +++ b/pkg/services/ngalert/remote/client/alertmanager_configuration.go @@ -11,7 +11,8 @@ import ( ) const ( - grafanaAlertmanagerConfigPath = "/api/v1/grafana/config" + grafanaAlertmanagerConfigPath = "/api/v1/grafana/config" + grafanaAlertmanagerReceiversPath = "/api/v1/grafana/receivers" ) type UserGrafanaConfig struct { @@ -63,3 +64,16 @@ func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimo func (mc *Mimir) DeleteGrafanaAlertmanagerConfig(ctx context.Context) error { return mc.doOK(ctx, grafanaAlertmanagerConfigPath, http.MethodDelete, nil) } + +func (mc *Mimir) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) { + response := []apimodels.Receiver{} + + // nolint:bodyclose + // closed within `do` + _, err := mc.do(ctx, grafanaAlertmanagerReceiversPath, http.MethodGet, nil, &response) + if err != nil { + return nil, err + } + + return response, nil +} diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index cf0a0941652..1fda8206c4d 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -29,6 +29,9 @@ type MimirClient interface { DeleteGrafanaAlertmanagerConfig(ctx context.Context) error ShouldPromoteConfig() bool + + // Mimir implements an extended version of the receivers API under a different path. + GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) } type Mimir struct { diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index 3a0114cb442..5f85f735b08 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -21,7 +21,7 @@ images = { "plugins_slack": "plugins/slack", "python": "python:3.8", "postgres_alpine": "postgres:12.3-alpine", - "mimir": "us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP", + "mimir": "grafana/mimir-alpine:r295-a23e559", "mysql5": "mysql:5.7.39", "mysql8": "mysql:8.0.32", "redis_alpine": "redis:6.2.11-alpine", From d46df10d3024c39db2369a91125853d88c322760 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 19 Jun 2024 13:33:29 +0200 Subject: [PATCH 019/122] DashboardScene: Show dashboard not found view (#89342) * DashboardScene: Show dashboard not found view * Test fix * Use correct selector --- .../src/selectors/components.ts | 3 + .../PageNotFound/EntityNotFound.tsx | 3 +- .../dashboard-scene/scene/DashboardScene.tsx | 4 ++ .../scene/DashboardSceneRenderer.test.tsx | 65 +++++++++++++++++++ .../scene/DashboardSceneRenderer.tsx | 24 +++++-- .../scene/NavToolbarActions.test.tsx | 22 ++++++- .../scene/NavToolbarActions.tsx | 2 +- .../dashboard/services/DashboardLoaderSrv.ts | 4 +- 8 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 3a763ec3fe5..677fe0d0d84 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -603,4 +603,7 @@ export const Components = { headerOrderSwitch: 'data-testid header-order-switch', headerPreviewSwitch: 'data-testid header-preview-switch', }, + EntityNotFound: { + container: 'data-testid entity-not-found', + }, }; diff --git a/public/app/core/components/PageNotFound/EntityNotFound.tsx b/public/app/core/components/PageNotFound/EntityNotFound.tsx index 24edfa76f5c..c749d30aa19 100644 --- a/public/app/core/components/PageNotFound/EntityNotFound.tsx +++ b/public/app/core/components/PageNotFound/EntityNotFound.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { EmptyState, TextLink, useStyles2 } from '@grafana/ui'; export interface Props { @@ -15,7 +16,7 @@ export function EntityNotFound({ entity = 'Page' }: Props) { const styles = useStyles2(getStyles); return ( -
+
We're looking but can't seem to find this {entity.toLowerCase()}. Try returning{' '} home or seeking help on the{' '} diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index e2bc1828f39..2b04bec57de 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -392,6 +392,10 @@ export class DashboardScene extends SceneObjectBase { public getPageNav(location: H.Location, navIndex: NavIndex) { const { meta, viewPanelScene, editPanel } = this.state; + if (meta.dashboardNotFound) { + return { text: 'Not found' }; + } + let pageNav: NavModelItem = { text: this.state.title, url: getDashboardUrl({ diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx new file mode 100644 index 00000000000..47d5cd076b3 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.test.tsx @@ -0,0 +1,65 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Router } from 'react-router'; +import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; + +import { selectors } from '@grafana/e2e-selectors'; +import { locationService } from '@grafana/runtime'; +import { GrafanaContext } from 'app/core/context/GrafanaContext'; +import { configureStore } from 'app/store/configureStore'; + +import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; + +describe('DashboardSceneRenderer', () => { + it('should render Not Found notice when dashboard is not found', async () => { + const scene = transformSaveModelToScene({ + meta: { + isSnapshot: true, + dashboardNotFound: true, + canStar: false, + canDelete: false, + canSave: false, + canEdit: false, + canShare: false, + }, + dashboard: { + title: 'Not found', + uid: 'uid', + schemaVersion: 0, + // Disabling build in annotations to avoid mocking Grafana data source + annotations: { + list: [ + { + builtIn: 1, + datasource: { + type: 'grafana', + uid: '-- Grafana --', + }, + enable: false, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + type: 'dashboard', + }, + ], + }, + }, + }); + + const store = configureStore({}); + const context = getGrafanaContextMock(); + + render( + + + + + + + + ); + + expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 553710923cf..7687efa6107 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -7,6 +7,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { SceneComponentProps } from '@grafana/scenes'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { getNavModel } from 'app/core/selectors/navModel'; import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty'; import { useSelector } from 'app/types'; @@ -35,14 +36,26 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps; + const emptyState = ( + + ); const withPanels = ( -
+
); + const notFound = meta.dashboardNotFound && ; + + let body = [withPanels]; + + if (notFound) { + body = [notFound]; + } else if (isEmpty) { + body = [emptyState, withPanels]; + } + return ( {editPanel && } @@ -55,7 +68,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps - {scopes && } + {scopes && !meta.dashboardNotFound && } {controls && hasControls && (
-
- <>{isEmpty && emptyState} - {withPanels} -
+
{body}
)} diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index 797a80fe51d..ce2bfb8fa66 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -8,6 +8,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; +import { DashboardMeta } from 'app/types'; import { buildPanelEditScene } from '../panel-edit/PanelEditor'; @@ -163,9 +164,27 @@ describe('NavToolbarActions', () => { expect(newShareButton).toBeInTheDocument(); }); }); + + describe('Snapshot', () => { + it('should show link button when is a snapshot', () => { + setup({ + isSnapshot: true, + }); + + expect(screen.queryByTestId('button-snapshot')).toBeInTheDocument(); + }); + it('should not show link button when is not found dashboard', () => { + setup({ + isSnapshot: true, + dashboardNotFound: true, + }); + + expect(screen.queryByTestId('button-snapshot')).not.toBeInTheDocument(); + }); + }); }); -function setup() { +function setup(meta?: DashboardMeta) { const dashboard = new DashboardScene({ $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), meta: { @@ -177,6 +196,7 @@ function setup() { canStar: true, canAdmin: true, canDelete: true, + ...meta, }, title: 'hello', uid: 'dash-1', diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 18a6a336ccf..47a714cc1dc 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -139,7 +139,7 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'icon-actions', - condition: meta.isSnapshot && !isEditing, + condition: meta.isSnapshot && !meta.dashboardNotFound && !isEditing, render: () => ( { - return this._dashboardLoadFailed('Not found', true); + const dash = this._dashboardLoadFailed('Not found', true); + dash.dashboard.uid = uid; + return dash; }); } else { throw new Error('Dashboard uid or slug required'); From b3907ca5ec3a4feee262dbe459bcf1b050a06bae Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 19 Jun 2024 13:55:31 +0200 Subject: [PATCH 020/122] Zanzana: Simple logger wrapper for openfga (#89396) * Zanzana: Simple logger wrapper for openfga * don't export --- pkg/services/authz/zanzana/logger.go | 86 ++++++++++++++++++++++++++++ pkg/services/authz/zanzana/server.go | 4 +- 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 pkg/services/authz/zanzana/logger.go diff --git a/pkg/services/authz/zanzana/logger.go b/pkg/services/authz/zanzana/logger.go new file mode 100644 index 00000000000..4f74c03284c --- /dev/null +++ b/pkg/services/authz/zanzana/logger.go @@ -0,0 +1,86 @@ +package zanzana + +import ( + "context" + + "go.uber.org/zap" + + "github.com/grafana/grafana/pkg/infra/log" +) + +// zanzanaLogger is a grafana logger wrapper compatible with OpenFGA logger interface +type zanzanaLogger struct { + logger *log.ConcreteLogger +} + +func newZanzanaLogger() *zanzanaLogger { + logger := log.New("openfga-server") + + return &zanzanaLogger{ + logger: logger, + } +} + +// Simple converter for zap logger fields +func zapFieldsToArgs(fields []zap.Field) []any { + args := make([]any, 0) + for _, f := range fields { + args = append(args, f.Key) + if f.Interface != nil { + args = append(args, f.Interface) + } else if f.String != "" { + args = append(args, f.String) + } else { + args = append(args, f.Integer) + } + } + return args +} + +func (l *zanzanaLogger) Debug(msg string, fields ...zap.Field) { + l.logger.Debug(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) Info(msg string, fields ...zap.Field) { + l.logger.Info(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) Warn(msg string, fields ...zap.Field) { + l.logger.Warn(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) Error(msg string, fields ...zap.Field) { + l.logger.Error(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) Panic(msg string, fields ...zap.Field) { + l.logger.Error(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) Fatal(msg string, fields ...zap.Field) { + l.logger.Error(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) DebugWithContext(ctx context.Context, msg string, fields ...zap.Field) { + l.logger.Debug(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) InfoWithContext(ctx context.Context, msg string, fields ...zap.Field) { + l.logger.Info(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) WarnWithContext(ctx context.Context, msg string, fields ...zap.Field) { + l.logger.Warn(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) ErrorWithContext(ctx context.Context, msg string, fields ...zap.Field) { + l.logger.Error(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) PanicWithContext(ctx context.Context, msg string, fields ...zap.Field) { + l.logger.Error(msg, zapFieldsToArgs(fields)...) +} + +func (l *zanzanaLogger) FatalWithContext(ctx context.Context, msg string, fields ...zap.Field) { + l.logger.Error(msg, zapFieldsToArgs(fields)...) +} diff --git a/pkg/services/authz/zanzana/server.go b/pkg/services/authz/zanzana/server.go index e00b2a22eb5..73c929252b4 100644 --- a/pkg/services/authz/zanzana/server.go +++ b/pkg/services/authz/zanzana/server.go @@ -1,7 +1,6 @@ package zanzana import ( - "github.com/openfga/openfga/pkg/logger" "github.com/openfga/openfga/pkg/server" "github.com/openfga/openfga/pkg/storage" ) @@ -10,8 +9,7 @@ func NewServer(store storage.OpenFGADatastore) (*server.Server, error) { // FIXME(kalleep): add support for more options, configure logging, tracing etc opts := []server.OpenFGAServiceV1Option{ server.WithDatastore(store), - // FIXME(kalleep): Write and log adapter for open fga logging interface - server.WithLogger(logger.NewNoopLogger()), + server.WithLogger(newZanzanaLogger()), } // FIXME(kalleep): Interceptors From 6b876f1e3845e0b5ec89dbf451e81d8fdec461bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Cazeaux?= Date: Wed, 19 Jun 2024 14:02:17 +0200 Subject: [PATCH 021/122] Prometheus: Fix interpolating adhoc filters with template variables (#88626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prometheus: replace variables on adhoc filters Fixes #87979 Signed-off-by: Stéphane Cazeaux * Prometheus: replace variable filters on adhoc variables also when promQLScope=true Signed-off-by: Stéphane Cazeaux --------- Signed-off-by: Stéphane Cazeaux --- .../grafana-prometheus/src/datasource.test.ts | 53 ++++++++++++++++++- packages/grafana-prometheus/src/datasource.ts | 28 ++++++++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/packages/grafana-prometheus/src/datasource.test.ts b/packages/grafana-prometheus/src/datasource.test.ts index dd94b4be299..08104df79b3 100644 --- a/packages/grafana-prometheus/src/datasource.test.ts +++ b/packages/grafana-prometheus/src/datasource.test.ts @@ -552,7 +552,7 @@ describe('PrometheusDatasource', () => { config.featureToggles.promQLScope = undefined; }); - it('should call replace function 2 times', () => { + it('should call replace function 3 times', () => { const query: PromQuery = { expr: 'test{job="testjob"}', format: 'time_series', @@ -563,7 +563,7 @@ describe('PrometheusDatasource', () => { replaceMock.mockReturnValue(interval); const queries = ds.interpolateVariablesInQueries([query], { Interval: { text: interval, value: interval } }); - expect(templateSrvStub.replace).toBeCalledTimes(2); + expect(templateSrvStub.replace).toBeCalledTimes(3); expect(queries[0].interval).toBe(interval); }); @@ -753,6 +753,55 @@ describe('PrometheusDatasource', () => { const result = ds.applyTemplateVariables(query, {}, filters); expect(result).toMatchObject({ expr: 'test{job="99", k1="v1", k2!="v2"} > 99' }); }); + + it('should replace variables in ad-hoc filters', () => { + const searchPattern = /\$A/g; + replaceMock.mockImplementation((a: string) => a?.replace(searchPattern, '99') ?? a); + + const query = { + expr: 'test', + refId: 'A', + }; + const filters = [ + { + key: 'job', + operator: '=~', + value: '$A', + }, + ]; + + const result = ds.applyTemplateVariables(query, {}, filters); + expect(result).toMatchObject({ expr: 'test{job=~"99"}' }); + }); + + it('should replace variables in adhoc filters on backend when promQLScope is enabled', () => { + config.featureToggles.promQLScope = true; + const searchPattern = /\$A/g; + replaceMock.mockImplementation((a: string) => a?.replace(searchPattern, '99') ?? a); + + const query = { + expr: 'test', + refId: 'A', + }; + const filters = [ + { + key: 'job', + operator: '=~', + value: '$A', + }, + ]; + const result = ds.applyTemplateVariables(query, {}, filters); + expect(result).toMatchObject({ + expr: 'test', + adhocFilters: [ + { + key: 'job', + operator: 'regex-match', + value: '99', + }, + ], + }); + }); }); describe('metricFindQuery', () => { diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index ac2e958eccf..34688f47465 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -659,14 +659,19 @@ export class PrometheusDatasource if (queries && queries.length) { expandedQueries = queries.map((query) => { const interpolatedQuery = this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr); + const replacedInterpolatedQuery = config.featureToggles.promQLScope + ? interpolatedQuery + : this.templateSrv.replace( + this.enhanceExprWithAdHocFilters(filters, interpolatedQuery), + scopedVars, + this.interpolateQueryExpr + ); const expandedQuery = { ...query, ...(config.featureToggles.promQLScope ? { adhocFilters: this.generateScopeFilters(filters) } : {}), datasource: this.getRef(), - expr: config.featureToggles.promQLScope - ? interpolatedQuery - : this.enhanceExprWithAdHocFilters(filters, interpolatedQuery), + expr: replacedInterpolatedQuery, interval: this.templateSrv.replace(query.interval, scopedVars), }; @@ -824,7 +829,11 @@ export class PrometheusDatasource return []; } - return filters.map((f) => ({ ...f, operator: scopeFilterOperatorMap[f.operator] })); + return filters.map((f) => ({ + ...f, + value: this.templateSrv.replace(f.value, {}, this.interpolateQueryExpr), + operator: scopeFilterOperatorMap[f.operator], + })); } enhanceExprWithAdHocFilters(filters: AdHocVariableFilter[] | undefined, expr: string) { @@ -865,12 +874,21 @@ export class PrometheusDatasource }; // interpolate expression + + // We need a first replace to evaluate variables before applying adhoc filters + // This is required for an expression like `metric > $VAR` where $VAR is a float to which we must not add adhoc filters const expr = this.templateSrv.replace(target.expr, variables, this.interpolateQueryExpr); + // Apply ad-hoc filters + // When ad-hoc filters are applied, we replace again the variables in case the ad-hoc filters also reference a variable + const exprWithAdhoc = config.featureToggles.promQLScope + ? expr + : this.templateSrv.replace(this.enhanceExprWithAdHocFilters(filters, expr), variables, this.interpolateQueryExpr); + return { ...target, ...(config.featureToggles.promQLScope ? { adhocFilters: this.generateScopeFilters(filters) } : {}), - expr: config.featureToggles.promQLScope ? expr : this.enhanceExprWithAdHocFilters(filters, expr), + expr: exprWithAdhoc, interval: this.templateSrv.replace(target.interval, variables), legendFormat: this.templateSrv.replace(target.legendFormat, variables), }; From ceaf510a7d949a0a653be68d61d06d1dd85ab07b Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 19 Jun 2024 13:28:00 +0100 Subject: [PATCH 022/122] Chore: SCSS cleanup (#89355) * remove/migrate normalize.scss * fix typo * migrate _explore styles * make json-formatter styles global --- .betterer.results | 4 - .../src/components/PromCheatSheet.tsx | 65 ++- .../src/components/PromExploreExtraField.tsx | 13 +- .../src/components/PromQueryField.tsx | 10 +- .../components/PromQueryBuilder.tsx | 8 +- .../src/themes/GlobalStyles/GlobalStyles.tsx | 2 + .../src/themes/GlobalStyles/code.ts | 3 +- .../src/themes/GlobalStyles/elements.ts | 80 ++++ .../src/themes/GlobalStyles/jsonFormatter.ts | 125 ++++++ .../components/CheatSheet/LogsCheatSheet.tsx | 45 +- .../query/influxql/InfluxCheatSheet.tsx | 39 +- .../loki/components/LokiCheatSheet.tsx | 75 ++-- public/sass/_angular.scss | 18 + public/sass/_grafana.scss | 3 - public/sass/base/_normalize.scss | 424 ------------------ public/sass/base/_reboot.scss | 2 +- public/sass/components/_json_explorer.scss | 122 ----- public/sass/pages/_explore.scss | 155 ------- 18 files changed, 404 insertions(+), 789 deletions(-) create mode 100644 packages/grafana-ui/src/themes/GlobalStyles/jsonFormatter.ts delete mode 100644 public/sass/base/_normalize.scss delete mode 100644 public/sass/components/_json_explorer.scss delete mode 100644 public/sass/pages/_explore.scss diff --git a/.betterer.results b/.betterer.results index 87b8a31de53..e14d97f73cc 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6134,10 +6134,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not re-export imported variable (\`./thirdArgAfterSearchQuery\`)", "4"], [0, 0, 0, "Do not re-export imported variable (\`./withinStringQuery\`)", "5"] ], - "public/app/plugins/datasource/cloudwatch/components/CheatSheet/LogsCheatSheet.tsx:5381": [ - [0, 0, 0, "Styles should be written using objects.", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"] - ], "public/app/plugins/datasource/cloudwatch/components/ConfigEditor/XrayLinkConfig.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], diff --git a/packages/grafana-prometheus/src/components/PromCheatSheet.tsx b/packages/grafana-prometheus/src/components/PromCheatSheet.tsx index 7eaee0b0782..4e452244cce 100644 --- a/packages/grafana-prometheus/src/components/PromCheatSheet.tsx +++ b/packages/grafana-prometheus/src/components/PromCheatSheet.tsx @@ -1,7 +1,9 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx +import { css } from '@emotion/css'; import React from 'react'; -import { QueryEditorHelpProps } from '@grafana/data'; +import { GrafanaTheme2, QueryEditorHelpProps } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; import { PromQuery } from '../types'; @@ -29,23 +31,44 @@ const CHEAT_SHEET_ITEMS = [ }, ]; -export const PromCheatSheet = (props: QueryEditorHelpProps) => ( -
-

PromQL Cheat Sheet

- {CHEAT_SHEET_ITEMS.map((item, index) => ( -
-
{item.title}
- {item.expression ? ( - - ) : null} -
{item.label}
-
- ))} -
-); +export const PromCheatSheet = (props: QueryEditorHelpProps) => { + const styles = useStyles2(getStyles); + + return ( +
+

PromQL Cheat Sheet

+ {CHEAT_SHEET_ITEMS.map((item, index) => ( +
+
{item.title}
+ {item.expression ? ( + + ) : null} + {item.label} +
+ ))} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + cheatSheetItem: css({ + margin: theme.spacing(3, 0), + }), + cheatSheetItemTitle: css({ + fontSize: theme.typography.h3.fontSize, + }), + cheatSheetExample: css({ + margin: theme.spacing(0.5, 0), + // element is interactive, clear button styles + textAlign: 'left', + border: 'none', + background: 'transparent', + display: 'block', + }), +}); diff --git a/packages/grafana-prometheus/src/components/PromExploreExtraField.tsx b/packages/grafana-prometheus/src/components/PromExploreExtraField.tsx index a202786e14a..bbff06629c5 100644 --- a/packages/grafana-prometheus/src/components/PromExploreExtraField.tsx +++ b/packages/grafana-prometheus/src/components/PromExploreExtraField.tsx @@ -4,7 +4,8 @@ import { isEqual } from 'lodash'; import React, { memo, useCallback } from 'react'; import { usePrevious } from 'react-use'; -import { InlineFormLabel, RadioButtonGroup } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { InlineFormLabel, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { PrometheusDatasource } from '../datasource'; import { PromQuery } from '../types'; @@ -21,6 +22,7 @@ export interface PromExploreExtraFieldProps { export const PromExploreExtraField = memo(({ query, datasource, onChange, onRunQuery }: PromExploreExtraFieldProps) => { const rangeOptions = getQueryTypeOptions(true); const prevQuery = usePrevious(query); + const styles = useStyles2(getStyles); const onExemplarChange = useCallback( (exemplar: boolean) => { @@ -59,7 +61,8 @@ export const PromExploreExtraField = memo(({ query, datasource, onChange, onRunQ
({ + queryTypeField: css({ + marginRight: theme.spacing(0.5), + }), +}); diff --git a/packages/grafana-prometheus/src/components/PromQueryField.tsx b/packages/grafana-prometheus/src/components/PromQueryField.tsx index ac648271d44..e4a83498aad 100644 --- a/packages/grafana-prometheus/src/components/PromQueryField.tsx +++ b/packages/grafana-prometheus/src/components/PromQueryField.tsx @@ -1,5 +1,5 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx -import { cx } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import React, { ReactNode } from 'react'; import { isDataFrame, QueryEditorProps, QueryHint, TimeRange, toLegacyResponseData } from '@grafana/data'; @@ -265,8 +265,12 @@ class PromQueryFieldClass extends React.PureComponent -
+
+
{hint.label}{' '} {hint.fix ? ( {isEditing ? ( -
-
- -
- {editMode === 'tz' ? ( -
- { - onToggleChangeTimeSettings(); - - if (isString(timeZone)) { - onChangeTimeZone(timeZone); - } - }} - onBlur={onToggleChangeTimeSettings} - menuShouldPortal={false} - /> -
- ) : ( -
- + + { + setEditMode('tz'); + }} + aria-controls={timeZoneSettingsId} + /> + { + setEditMode('fy'); + }} + aria-controls={fiscalYearSettingsId} + /> + + + {editMode === 'tz' ? ( +
- { + if (onChangeFiscalYearStartMonth) { + onChangeFiscalYearStartMonth(value.value ?? 0); + } + }} + /> + +
+ )} +
) : null}
@@ -143,7 +164,7 @@ export const TimePickerFooter = (props: Props) => { const getStyle = (theme: GrafanaTheme2) => ({ container: css({ borderTop: `1px solid ${theme.colors.border.weak}`, - padding: '11px', + padding: theme.spacing(1.5), display: 'flex', flexDirection: 'row', justifyContent: 'space-between', @@ -151,7 +172,8 @@ const getStyle = (theme: GrafanaTheme2) => ({ }), editContainer: css({ borderTop: `1px solid ${theme.colors.border.weak}`, - padding: '11px', + padding: theme.spacing(1.5), + paddingTop: 0, justifyContent: 'space-between', alignItems: 'center', }), From fa3679d29b78119af96608bec434c0c8664cf28d Mon Sep 17 00:00:00 2001 From: jvaelen Date: Wed, 19 Jun 2024 17:01:02 +0200 Subject: [PATCH 033/122] Docs: correct tracesToLogsV2 example span time shifts (#88421) Co-authored-by: Jack Baldry --- docs/sources/datasources/tempo/configure-tempo-data-source.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/datasources/tempo/configure-tempo-data-source.md b/docs/sources/datasources/tempo/configure-tempo-data-source.md index cc54fae4c44..93fe169a7ba 100644 --- a/docs/sources/datasources/tempo/configure-tempo-data-source.md +++ b/docs/sources/datasources/tempo/configure-tempo-data-source.md @@ -273,8 +273,8 @@ datasources: # Field with an internal link pointing to a logs data source in Grafana. # datasourceUid value must match the uid value of the logs data source. datasourceUid: 'loki' - spanStartTimeShift: '1h' - spanEndTimeShift: '-1h' + spanStartTimeShift: '-1h' + spanEndTimeShift: '1h' tags: ['job', 'instance', 'pod', 'namespace'] filterByTraceID: false filterBySpanID: false From 13e16a4142d9a210ea21f9cac3491284a30d2187 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:16:04 +0300 Subject: [PATCH 034/122] Transformations: Fix filter by value error on interpolation (#89418) * string check before interpolation * refactor --- .../transformers/filterByValue.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/filterByValue.ts b/packages/grafana-data/src/transformations/transformers/filterByValue.ts index 39e80a2d824..3305547bd10 100644 --- a/packages/grafana-data/src/transformations/transformers/filterByValue.ts +++ b/packages/grafana-data/src/transformations/transformers/filterByValue.ts @@ -56,8 +56,12 @@ export const filterByValueTransformer: DataTransformerInfo { if (filter.config.id === ValueMatcherID.between) { - const interpolatedFrom = ctx.interpolate(filter.config.options.from); - const interpolatedTo = ctx.interpolate(filter.config.options.to); + if (typeof filter.config.options.from === 'string') { + filter.config.options.from = ctx.interpolate(filter.config.options.from); + } + if (typeof filter.config.options.to === 'string') { + filter.config.options.to = ctx.interpolate(filter.config.options.to); + } const newFilter = { ...filter, @@ -65,8 +69,8 @@ export const filterByValueTransformer: DataTransformerInfo Date: Wed, 19 Jun 2024 11:12:11 -0500 Subject: [PATCH 035/122] Table: Don't wrap text labels for gauge cells (#88005) Don't wrap text Co-authored-by: nmarrs --- packages/grafana-ui/src/components/BarGauge/BarGauge.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx index cf4a1d5905c..db2ff91f09b 100644 --- a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -657,6 +657,7 @@ function getValueStyles( width: `${width}px`, display: 'flex', alignItems: 'center', + textWrap: 'nowrap', lineHeight: VALUE_LINE_HEIGHT, }; From 92f10b73a834d808e13f70c15904e135f98c8721 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 19 Jun 2024 16:18:33 -0400 Subject: [PATCH 036/122] Alerting: Move interface Namespaced from accesscontrol to models package (#89439) move Namespaced interface from accesscontrol to models --- pkg/services/ngalert/accesscontrol/fakes/rules.go | 9 ++++----- pkg/services/ngalert/accesscontrol/rules.go | 8 ++------ pkg/services/ngalert/api/api.go | 2 +- pkg/services/ngalert/api/testing.go | 3 +-- pkg/services/ngalert/models/alert_rule.go | 5 +++++ pkg/services/ngalert/notifier/silence_svc.go | 3 +-- pkg/services/ngalert/notifier/silence_svc_test.go | 9 ++++----- pkg/services/ngalert/provisioning/accesscontrol.go | 3 +-- pkg/services/ngalert/provisioning/accesscontrol_test.go | 5 ++--- pkg/services/ngalert/provisioning/alert_rules_test.go | 4 ++-- pkg/services/ngalert/provisioning/testing.go | 3 +-- 11 files changed, 24 insertions(+), 30 deletions(-) diff --git a/pkg/services/ngalert/accesscontrol/fakes/rules.go b/pkg/services/ngalert/accesscontrol/fakes/rules.go index f666419beca..52989e44eab 100644 --- a/pkg/services/ngalert/accesscontrol/fakes/rules.go +++ b/pkg/services/ngalert/accesscontrol/fakes/rules.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" ) @@ -22,8 +21,8 @@ type FakeRuleService struct { AuthorizeDatasourceAccessForRuleGroupFunc func(context.Context, identity.Requester, models.RulesGroup) error HasAccessToRuleGroupFunc func(context.Context, identity.Requester, models.RulesGroup) (bool, error) AuthorizeAccessToRuleGroupFunc func(context.Context, identity.Requester, models.RulesGroup) error - HasAccessInFolderFunc func(context.Context, identity.Requester, accesscontrol.Namespaced) (bool, error) - AuthorizeAccessInFolderFunc func(context.Context, identity.Requester, accesscontrol.Namespaced) error + HasAccessInFolderFunc func(context.Context, identity.Requester, models.Namespaced) (bool, error) + AuthorizeAccessInFolderFunc func(context.Context, identity.Requester, models.Namespaced) error AuthorizeRuleChangesFunc func(context.Context, identity.Requester, *store.GroupDelta) error Calls []Call @@ -77,7 +76,7 @@ func (s *FakeRuleService) AuthorizeAccessToRuleGroup(ctx context.Context, user i return nil } -func (s *FakeRuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) (bool, error) { +func (s *FakeRuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) (bool, error) { s.Calls = append(s.Calls, Call{"HasAccessInFolder", []interface{}{ctx, user, namespaced}}) if s.HasAccessInFolderFunc != nil { return s.HasAccessInFolderFunc(ctx, user, namespaced) @@ -85,7 +84,7 @@ func (s *FakeRuleService) HasAccessInFolder(ctx context.Context, user identity.R return false, nil } -func (s *FakeRuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error { +func (s *FakeRuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error { s.Calls = append(s.Calls, Call{"AuthorizeAccessInFolder", []interface{}{ctx, user, namespaced}}) if s.AuthorizeAccessInFolderFunc != nil { return s.AuthorizeAccessInFolderFunc(ctx, user, namespaced) diff --git a/pkg/services/ngalert/accesscontrol/rules.go b/pkg/services/ngalert/accesscontrol/rules.go index 998b30f56a0..2b8e2fd2914 100644 --- a/pkg/services/ngalert/accesscontrol/rules.go +++ b/pkg/services/ngalert/accesscontrol/rules.go @@ -31,10 +31,6 @@ func NewRuleService(ac accesscontrol.AccessControl) *RuleService { } } -type Namespaced interface { - GetNamespaceUID() string -} - // getReadFolderAccessEvaluator constructs accesscontrol.Evaluator that checks all permissions required to read rules in specific folder func getReadFolderAccessEvaluator(folderUID string) accesscontrol.Evaluator { return accesscontrol.EvalAll( @@ -130,7 +126,7 @@ func (r *RuleService) AuthorizeAccessToRuleGroup(ctx context.Context, user ident // - ("folders:read") read the folder // - ("alert.rules:read") read alert rules in the folder // Returns false if the requester does not have enough permissions, and error if something went wrong during the permission evaluation. -func (r *RuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, rule Namespaced) (bool, error) { +func (r *RuleService) HasAccessInFolder(ctx context.Context, user identity.Requester, rule models.Namespaced) (bool, error) { eval := accesscontrol.EvalAll(getReadFolderAccessEvaluator(rule.GetNamespaceUID())) return r.HasAccess(ctx, user, eval) } @@ -140,7 +136,7 @@ func (r *RuleService) HasAccessInFolder(ctx context.Context, user identity.Reque // - ("folders:read") read the folder // - ("alert.rules:read") read alert rules in the folder // Returns error if at least one permission is missing or if something went wrong during the permission evaluation -func (r *RuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, rule Namespaced) error { +func (r *RuleService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, rule models.Namespaced) error { eval := accesscontrol.EvalAll(getReadFolderAccessEvaluator(rule.GetNamespaceUID())) return r.HasAccessOrError(ctx, user, eval, func() string { return fmt.Sprintf("access rules in folder '%s'", rule.GetNamespaceUID()) diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index df99909b9b4..1b40ec4a2d9 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -45,7 +45,7 @@ type RuleAccessControlService interface { AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error AuthorizeDatasourceAccessForRule(ctx context.Context, user identity.Requester, rule *models.AlertRule) error AuthorizeDatasourceAccessForRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error - AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error + AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error } // API handlers. diff --git a/pkg/services/ngalert/api/testing.go b/pkg/services/ngalert/api/testing.go index 6cb35aa101e..3a84fbd6952 100644 --- a/pkg/services/ngalert/api/testing.go +++ b/pkg/services/ngalert/api/testing.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" @@ -147,7 +146,7 @@ func (f fakeRuleAccessControlService) AuthorizeAccessToRuleGroup(ctx context.Con return nil } -func (f fakeRuleAccessControlService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error { +func (f fakeRuleAccessControlService) AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error { return nil } diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index b505bf87ca6..b379810b579 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -269,6 +269,11 @@ type AlertRule struct { NotificationSettings []NotificationSettings `xorm:"notification_settings"` // we use slice to workaround xorm mapping that does not serialize a struct to JSON unless it's a slice } +// Namespaced describes a class of resources that are stored in a specific namespace. +type Namespaced interface { + GetNamespaceUID() string +} + // AlertRuleWithOptionals This is to avoid having to pass in additional arguments deep in the call stack. Alert rule // object is created in an early validation step without knowledge about current alert rule fields or if they need to be // overridden. This is done in a later step and, in that step, we did not have knowledge about if a field was optional diff --git a/pkg/services/ngalert/notifier/silence_svc.go b/pkg/services/ngalert/notifier/silence_svc.go index dd40f11015e..aab960a4bcd 100644 --- a/pkg/services/ngalert/notifier/silence_svc.go +++ b/pkg/services/ngalert/notifier/silence_svc.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/models" ) @@ -24,7 +23,7 @@ type SilenceService struct { } type RuleAccessControlService interface { - HasAccessInFolder(ctx context.Context, user identity.Requester, rule accesscontrol.Namespaced) (bool, error) + HasAccessInFolder(ctx context.Context, user identity.Requester, rule models.Namespaced) (bool, error) } // SilenceAccessControlService provides access control for silences. diff --git a/pkg/services/ngalert/notifier/silence_svc_test.go b/pkg/services/ngalert/notifier/silence_svc_test.go index 5bfcf7d05cd..6e81f98bcbc 100644 --- a/pkg/services/ngalert/notifier/silence_svc_test.go +++ b/pkg/services/ngalert/notifier/silence_svc_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" @@ -61,7 +60,7 @@ func TestWithRuleMetadata(t *testing.T) { user := ac.BackgroundUser("test", 1, org.RoleNone, nil) t.Run("Attach rule metadata to silences", func(t *testing.T) { ruleAuthz := fakes.FakeRuleService{} - ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) { + ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence models.Namespaced) (bool, error) { return true, nil } @@ -95,7 +94,7 @@ func TestWithRuleMetadata(t *testing.T) { }) t.Run("Don't attach full rule metadata if no access or global", func(t *testing.T) { ruleAuthz := fakes.FakeRuleService{} - ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) { + ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence models.Namespaced) (bool, error) { return silence.GetNamespaceUID() == "folder1", nil } @@ -134,7 +133,7 @@ func TestWithRuleMetadata(t *testing.T) { }) t.Run("Don't check same namespace access more than once", func(t *testing.T) { ruleAuthz := fakes.FakeRuleService{} - ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence accesscontrol.Namespaced) (bool, error) { + ruleAuthz.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, silence models.Namespaced) (bool, error) { return true, nil } @@ -159,7 +158,7 @@ func TestWithRuleMetadata(t *testing.T) { require.NoError(t, svc.WithRuleMetadata(context.Background(), user, silencesWithMetadata...)) assert.Lenf(t, ruleAuthz.Calls, 1, "HasAccessInFolder should be called only once per namespace") assert.Equal(t, "HasAccessInFolder", ruleAuthz.Calls[0].MethodName) - assert.Equal(t, "folder1", ruleAuthz.Calls[0].Arguments[2].(accesscontrol.Namespaced).GetNamespaceUID()) + assert.Equal(t, "folder1", ruleAuthz.Calls[0].Arguments[2].(models.Namespaced).GetNamespaceUID()) }) } diff --git a/pkg/services/ngalert/provisioning/accesscontrol.go b/pkg/services/ngalert/provisioning/accesscontrol.go index 8e503efadc1..97ae4cc6294 100644 --- a/pkg/services/ngalert/provisioning/accesscontrol.go +++ b/pkg/services/ngalert/provisioning/accesscontrol.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" ) @@ -13,7 +12,7 @@ import ( type RuleAccessControlService interface { HasAccess(ctx context.Context, user identity.Requester, evaluator ac.Evaluator) (bool, error) AuthorizeAccessToRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error - AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error + AuthorizeAccessInFolder(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error AuthorizeRuleChanges(ctx context.Context, user identity.Requester, change *store.GroupDelta) error } diff --git a/pkg/services/ngalert/provisioning/accesscontrol_test.go b/pkg/services/ngalert/provisioning/accesscontrol_test.go index 3f771ddfa3f..8df47a15634 100644 --- a/pkg/services/ngalert/provisioning/accesscontrol_test.go +++ b/pkg/services/ngalert/provisioning/accesscontrol_test.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/accesscontrol" - accesscontrol2 "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -200,7 +199,7 @@ func TestAuthorizeAccessToRule(t *testing.T) { rs.HasAccessFunc = func(ctx context.Context, user identity.Requester, evaluator accesscontrol.Evaluator) (bool, error) { return false, nil } - rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced accesscontrol2.Namespaced) error { + rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, namespaced models.Namespaced) error { return nil } @@ -231,7 +230,7 @@ func TestAuthorizeAccessToRule(t *testing.T) { return false, nil } expected = errors.New("test2") - rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, rule accesscontrol2.Namespaced) error { + rs.AuthorizeAccessInFolderFunc = func(ctx context.Context, requester identity.Requester, rule models.Namespaced) error { return expected } diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 1fca30486fd..8786acc63db 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -1020,7 +1020,7 @@ func TestGetAlertRule(t *testing.T) { service, _, _, ac := initServiceWithData(t) expected := errors.New("test") - ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error { + ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error { assert.Equal(t, u, user) assert.EqualValues(t, rule, namespaced) return expected @@ -1034,7 +1034,7 @@ func TestGetAlertRule(t *testing.T) { assert.Equal(t, "AuthorizeRuleRead", ac.Calls[0].Method) ac.Calls = nil - ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error { + ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error { return nil } diff --git a/pkg/services/ngalert/provisioning/testing.go b/pkg/services/ngalert/provisioning/testing.go index a61bdcc00c0..13422e79b50 100644 --- a/pkg/services/ngalert/provisioning/testing.go +++ b/pkg/services/ngalert/provisioning/testing.go @@ -9,7 +9,6 @@ import ( mock "github.com/stretchr/testify/mock" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -161,7 +160,7 @@ type fakeRuleAccessControlService struct { mu sync.Mutex Calls []call AuthorizeAccessToRuleGroupFunc func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error - AuthorizeAccessInFolderFunc func(ctx context.Context, user identity.Requester, namespaced accesscontrol.Namespaced) error + AuthorizeAccessInFolderFunc func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error AuthorizeRuleChangesFunc func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error CanReadAllRulesFunc func(ctx context.Context, user identity.Requester) (bool, error) CanWriteAllRulesFunc func(ctx context.Context, user identity.Requester) (bool, error) From 5f44c97e5fda71d7597d62372802483598ba2cbd Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 20 Jun 2024 09:10:03 +0200 Subject: [PATCH 037/122] API Servers: Avoid crashing on panic (#89281) --- pkg/apiserver/builder/helper.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/apiserver/builder/helper.go b/pkg/apiserver/builder/helper.go index 1fe320aceb5..195ff60f54a 100644 --- a/pkg/apiserver/builder/helper.go +++ b/pkg/apiserver/builder/helper.go @@ -14,6 +14,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/version" openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" "k8s.io/apiserver/pkg/registry/generic" @@ -99,6 +100,8 @@ func SetupConfig( handler = filters.WithAcceptHeader(handler) handler = filters.WithPathRewriters(handler, pathRewriters) handler = k8stracing.WithTracing(handler, serverConfig.TracerProvider, "KubernetesAPI") + // Configure filters.WithPanicRecovery to not crash on panic + utilruntime.ReallyCrash = false return handler } From 76c1750d38a64f4e8a64c6ab520c3d2670ee73f6 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Thu, 20 Jun 2024 08:17:53 +0100 Subject: [PATCH 038/122] Tempo: Remove kind=server from metrics summary (#89419) * Update query / messaging * Update field * Update test --- .../SearchTraceQLEditor/GroupByField.tsx | 5 +--- .../plugins/datasource/tempo/datasource.ts | 4 +--- .../datasource/tempo/metricsSummary.test.ts | 24 ++++--------------- .../datasource/tempo/metricsSummary.ts | 10 ++------ 4 files changed, 9 insertions(+), 34 deletions(-) diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx index 6f235c62260..4f93c941b18 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx @@ -71,10 +71,7 @@ export const GroupByField = (props: Props) => { const scopeOptions = Object.values(TraceqlSearchScope).map((t) => ({ label: t, value: t })); return ( - + <> {query.groupBy?.map((f, i) => { const tags = getTags(f) diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index ff195732714..bcf330a29ec 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -536,9 +536,7 @@ export class TempoDatasource extends DataSourceWithBackend { "datasourceName": "tempo", "datasourceUid": "gdev-tempo", "query": { - "query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]} && kind=server} | by(resource.service.name)", + "query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]}} | by(resource.service.name)", "queryType": "traceql", }, }, @@ -83,7 +83,7 @@ describe('MetricsSummary', () => { "datasourceName": "tempo", "datasourceUid": "gdev-tempo", "query": { - "query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]} && kind=server} | by(resource.service.name)", + "query": "{name="HTTP POST - post" && span.http.status_code=\${__data.fields["span.http.status_code"]} && temperature=\${__data.fields["temperature"]}} | by(resource.service.name)", "queryType": "traceql", }, }, @@ -99,19 +99,6 @@ describe('MetricsSummary', () => { 38.1, ], }, - { - "config": { - "custom": { - "width": 150, - }, - "displayNameFromDS": "Kind", - }, - "name": "kind", - "type": "string", - "values": [ - "server", - ], - }, { "config": { "custom": { @@ -222,7 +209,6 @@ describe('MetricsSummary', () => { { "contains_sink": "true", "errorPercentage": 10, - "kind": "server", "p50": 1, "p90": 2, "p95": 3, @@ -241,21 +227,21 @@ describe('MetricsSummary', () => { it('getConfigQuery should return correctly for empty target query', () => { const result = getConfigQuery(series, '{}'); expect(result).toEqual( - '{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]} && kind=server}' + '{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}}' ); }); it('getConfigQuery should return correctly for target query', () => { const result = getConfigQuery(series, '{name="HTTP POST - post"} | by(resource.service.name)'); expect(result).toEqual( - '{name="HTTP POST - post" && span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]} && kind=server} | by(resource.service.name)' + '{name="HTTP POST - post" && span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}} | by(resource.service.name)' ); }); it('getConfigQuery should return correctly for target query without brackets', () => { const result = getConfigQuery(series, 'by(resource.service.name)'); expect(result).toEqual( - '{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]} && kind=server} | by(resource.service.name)' + '{span.http.status_code=${__data.fields["span.http.status_code"]} && temperature=${__data.fields["temperature"]} && room="${__data.fields["room"]}" && contains_sink="${__data.fields["contains_sink"]}" && window_open="${__data.fields["window_open"]}" && spanStatus=${__data.fields["spanStatus"]} && spanKind=${__data.fields["spanKind"]}} | by(resource.service.name)' ); }); }); diff --git a/public/app/plugins/datasource/tempo/metricsSummary.ts b/public/app/plugins/datasource/tempo/metricsSummary.ts index e0b9c4ea23b..9aebb32853c 100644 --- a/public/app/plugins/datasource/tempo/metricsSummary.ts +++ b/public/app/plugins/datasource/tempo/metricsSummary.ts @@ -70,11 +70,6 @@ export function createTableFrameFromMetricsSummaryQuery( refId: 'metrics-summary', fields: [ ...Object.values(dynamicMetrics).sort((a, b) => a.name.localeCompare(b.name)), - { - name: 'kind', - type: FieldType.string, - config: { displayNameFromDS: 'Kind', custom: { width: 150 } }, - }, { name: 'spanCount', type: FieldType.number, @@ -113,7 +108,6 @@ export const transformToMetricsData = (data: MetricsSummary) => { : '0%'; const metricsData: MetricsData = { - kind: 'server', // so the user knows all results are of kind = server spanCount: getNumberForMetric(data.spanCount), errorPercentage, p50: getNumberForMetric(data.p50), @@ -145,12 +139,12 @@ export const getConfigQuery = (series: Series[], targetQuery: string) => { configQuery = targetQuery.substring(0, closingBracketIndex); if (queryParts.length > 0) { configQuery += targetQuery.replace(/\s/g, '').includes('{}') ? '' : ' && '; - configQuery += `${queryParts.join(' && ')} && kind=server`; + configQuery += `${queryParts.join(' && ')}`; configQuery += `}`; } configQuery += `${queryAfterClosingBracket}`; } else { - configQuery = `{${queryParts.join(' && ')} && kind=server} | ${targetQuery}`; + configQuery = `{${queryParts.join(' && ')}} | ${targetQuery}`; } return configQuery; From df987fe2e5be4ba66db1cb22cd1a4d845a9e4d31 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 20 Jun 2024 09:43:46 +0200 Subject: [PATCH 039/122] PanelChrome: Use labelledby for accessible title (#88781) * Add labelledby to PanelChrome * Fix React's subpar useId * No labelledby if there is no title --- .../src/components/PanelChrome/PanelChrome.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index ef72c19eca9..1aeff49e69f 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -135,6 +135,7 @@ export function PanelChrome({ const theme = useTheme2(); const styles = useStyles2(getStyles); const panelContentId = useId(); + const panelTitleId = useId().replace(/:/g, '_'); const hasHeader = !hoverHeader; @@ -179,7 +180,13 @@ export function PanelChrome({ {/* Non collapsible title */} {!collapsible && title && (
- + {title}
@@ -206,7 +213,7 @@ export function PanelChrome({ aria-hidden={!!title} aria-label={!title ? 'toggle collapse panel' : undefined} /> - + {title} @@ -249,6 +256,7 @@ export function PanelChrome({
Date: Thu, 20 Jun 2024 10:37:16 +0200 Subject: [PATCH 040/122] Zanzana: Simple openfga client wrapper (#89430) --- pkg/services/authz/zanzana.go | 10 +++---- pkg/services/authz/zanzana/client.go | 40 ++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index a1a48090c69..5533aa4a0d5 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -21,25 +21,23 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -type ZanzanaClient interface{} - // ProvideZanzana used to register ZanzanaClient. // It will also start an embedded ZanzanaSever if mode is set to "embedded". -func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (ZanzanaClient, error) { +func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureToggles) (zanzana.Client, error) { if !features.IsEnabledGlobally(featuremgmt.FlagZanzana) { return zanzana.NoopClient{}, nil } logger := log.New("zanzana") - var client *zanzana.Client + var client zanzana.Client switch cfg.Zanzana.Mode { case setting.ZanzanaModeClient: conn, err := grpc.NewClient(cfg.Zanzana.Addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) } - client = zanzana.NewClient(openfgav1.NewOpenFGAServiceClient(conn)) + client = zanzana.NewClient(conn) case setting.ZanzanaModeEmbedded: store, err := zanzana.NewEmbeddedStore(cfg, db, logger) if err != nil { @@ -53,7 +51,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, features featuremgmt.FeatureTogg channel := &inprocgrpc.Channel{} openfgav1.RegisterOpenFGAServiceServer(channel, srv) - client = zanzana.NewClient(openfgav1.NewOpenFGAServiceClient(channel)) + client = zanzana.NewClient(channel) default: return nil, fmt.Errorf("unsupported zanzana mode: %s", cfg.Zanzana.Mode) } diff --git a/pkg/services/authz/zanzana/client.go b/pkg/services/authz/zanzana/client.go index e52a39a8be6..e7a093fd426 100644 --- a/pkg/services/authz/zanzana/client.go +++ b/pkg/services/authz/zanzana/client.go @@ -1,16 +1,46 @@ package zanzana import ( + "context" + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "google.golang.org/grpc" + + "github.com/grafana/grafana/pkg/infra/log" ) -// FIXME(kalleep): Build out our wrapper client for openFGA -type Client struct { - c openfgav1.OpenFGAServiceClient +// Client is a wrapper around OpenFGAServiceClient with only methods using in Grafana included. +type Client interface { + Check(ctx context.Context, in *openfgav1.CheckRequest, opts ...grpc.CallOption) (*openfgav1.CheckResponse, error) + ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest, opts ...grpc.CallOption) (*openfgav1.ListObjectsResponse, error) } -func NewClient(c openfgav1.OpenFGAServiceClient) *Client { - return &Client{c} +type zanzanaClient struct { + client openfgav1.OpenFGAServiceClient + logger log.Logger +} + +func NewClient(cc grpc.ClientConnInterface) Client { + return &zanzanaClient{ + client: openfgav1.NewOpenFGAServiceClient(cc), + logger: log.New("zanzana-client"), + } +} + +func (c *zanzanaClient) Check(ctx context.Context, in *openfgav1.CheckRequest, opts ...grpc.CallOption) (*openfgav1.CheckResponse, error) { + return c.client.Check(ctx, in, opts...) +} + +func (c *zanzanaClient) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest, opts ...grpc.CallOption) (*openfgav1.ListObjectsResponse, error) { + return c.client.ListObjects(ctx, in, opts...) } type NoopClient struct{} + +func (nc NoopClient) Check(ctx context.Context, in *openfgav1.CheckRequest, opts ...grpc.CallOption) (*openfgav1.CheckResponse, error) { + return nil, nil +} + +func (nc NoopClient) ListObjects(ctx context.Context, in *openfgav1.ListObjectsRequest, opts ...grpc.CallOption) (*openfgav1.ListObjectsResponse, error) { + return nil, nil +} From f04abb414d6ec6e79bebb87b0a9b9f83623b6eb7 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 20 Jun 2024 11:49:23 +0300 Subject: [PATCH 041/122] Scopes: Fix preserving scopes when accessing a dashboard only by uid (#89406) --- .../scene/Scopes/ScopesFiltersScene.tsx | 12 ++++++-- .../scene/Scopes/ScopesScene.tsx | 2 +- .../dashboard-scene/scene/Scopes/api.ts | 24 ++-------------- .../dashboard-scene/scene/Scopes/utils.ts | 28 +++++++++++++++++++ 4 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/Scopes/utils.ts diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx index cef966c5944..fa2213265f5 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx @@ -21,6 +21,7 @@ import { ScopesScene } from './ScopesScene'; import { ScopesTreeLevel } from './ScopesTreeLevel'; import { fetchNodes, fetchScope, fetchSelectedScopes } from './api'; import { NodesMap, SelectedScope, TreeScope } from './types'; +import { getBasicScope } from './utils'; export interface ScopesFiltersSceneState extends SceneObjectState { nodes: NodesMap; @@ -180,9 +181,16 @@ export class ScopesFiltersScene extends SceneObjectBase return; } - this.setState({ treeScopes, isLoadingScopes: true }); + this.setState({ + // Update the scopes with the basic scopes otherwise they'd be lost between URL syncs + scopes: treeScopes.map(({ scopeName, path }) => ({ scope: getBasicScope(scopeName), path })), + treeScopes, + isLoadingScopes: true, + }); - this.setState({ scopes: await fetchSelectedScopes(treeScopes), isLoadingScopes: false }); + const scopes = await fetchSelectedScopes(treeScopes); + + this.setState({ scopes, isLoadingScopes: false }); } public resetDirtyScopeNames() { diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.tsx index a7f991913c5..f0217190b57 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.tsx @@ -30,7 +30,7 @@ export class ScopesScene extends SceneObjectBase { this.addActivationHandler(() => { this._subs.add( this.state.filters.subscribeToState((newState, prevState) => { - if (newState.scopes !== prevState.scopes) { + if (!newState.isLoadingScopes && newState.scopes !== prevState.scopes) { if (this.state.isExpanded) { this.state.dashboards.fetchDashboards(this.state.filters.getSelectedScopes()); } diff --git a/public/app/features/dashboard-scene/scene/Scopes/api.ts b/public/app/features/dashboard-scene/scene/Scopes/api.ts index 71ec07cec19..07df4877a54 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/api.ts +++ b/public/app/features/dashboard-scene/scene/Scopes/api.ts @@ -3,6 +3,7 @@ import { config, getBackendSrv } from '@grafana/runtime'; import { ScopedResourceClient } from 'app/features/apiserver/client'; import { NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types'; +import { getBasicScope, mergeScopes } from './utils'; const group = 'scope.grafana.app'; const version = 'v0alpha1'; @@ -48,31 +49,12 @@ export async function fetchScope(name: string): Promise { } const response = new Promise(async (resolve) => { - const basicScope: Scope = { - metadata: { name }, - spec: { - filters: [], - title: name, - type: '', - category: '', - description: '', - }, - }; + const basicScope = getBasicScope(name); try { const serverScope = await scopesClient.get(name); - const scope = { - ...basicScope, - metadata: { - ...basicScope.metadata, - ...serverScope.metadata, - }, - spec: { - ...basicScope.spec, - ...serverScope.spec, - }, - }; + const scope = mergeScopes(basicScope, serverScope); resolve(scope); } catch (err) { diff --git a/public/app/features/dashboard-scene/scene/Scopes/utils.ts b/public/app/features/dashboard-scene/scene/Scopes/utils.ts new file mode 100644 index 00000000000..bbe68774966 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/Scopes/utils.ts @@ -0,0 +1,28 @@ +import { Scope } from '@grafana/data'; + +export function getBasicScope(name: string): Scope { + return { + metadata: { name }, + spec: { + filters: [], + title: name, + type: '', + category: '', + description: '', + }, + }; +} + +export function mergeScopes(scope1: Scope, scope2: Scope): Scope { + return { + ...scope1, + metadata: { + ...scope1.metadata, + ...scope2.metadata, + }, + spec: { + ...scope1.spec, + ...scope2.spec, + }, + }; +} From 52a824f4ed6a23f74ddaf89ab13f74b8ff308799 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:53:56 +0300 Subject: [PATCH 042/122] Update dependency @grafana/scenes to v5.1.2 (#89454) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/yarn.lock b/yarn.lock index 079e080a949..7b3e11f200b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3555,8 +3555,8 @@ __metadata: linkType: soft "@grafana/scenes@npm:^5.0.2": - version: 5.0.3 - resolution: "@grafana/scenes@npm:5.0.3" + version: 5.1.2 + resolution: "@grafana/scenes@npm:5.1.2" dependencies: "@grafana/e2e-selectors": "npm:^11.0.0" "@leeoniya/ufuzzy": "npm:^1.0.14" @@ -3571,7 +3571,7 @@ __metadata: "@grafana/ui": ^10.4.1 react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/d99e88ba26f6df34fa595656be20cdaee8e9e59a8928e80691bea5cd9646ef6534c9929f38a5fcca1b09891a2609d4ee8e96f898799c5ec73b1993abe09b094a + checksum: 10/814fe81537d267640cf0e4d91c1fc5805290fd6f46bbf37633edfbc8fbeae8a064a2b9540d482adb061235bd20148ddf246db6e41a8141380e98d49ca31638f3 languageName: node linkType: hard @@ -14704,7 +14704,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.2.0, entities@npm:^4.3.0, entities@npm:^4.4.0": +"entities@npm:^4.2.0, entities@npm:^4.4.0": version: 4.4.0 resolution: "entities@npm:4.4.0" checksum: 10/b627cb900e901cc7817037b83bf993a1cbf6a64850540f7526af7bcf9c7d09ebc671198e6182cfae4680f733799e2852e6a1c46aa62ff36eb99680057a038df5 @@ -17883,19 +17883,7 @@ __metadata: languageName: node linkType: hard -"htmlparser2@npm:^8.0.1": - version: 8.0.1 - resolution: "htmlparser2@npm:8.0.1" - dependencies: - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.2" - domutils: "npm:^3.0.1" - entities: "npm:^4.3.0" - checksum: 10/f891041c331ef7ef300f1e8f0e6756d663cf8096f8a343a1bf474e7a5ce34fe7cd71b9dfb0227277f7de2007e847ef2a447e8b48eab592d6f3631aae18301d22 - languageName: node - linkType: hard - -"htmlparser2@npm:^8.0.2": +"htmlparser2@npm:^8.0.1, htmlparser2@npm:^8.0.2": version: 8.0.2 resolution: "htmlparser2@npm:8.0.2" dependencies: @@ -24769,7 +24757,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:2.3.1": +"punycode@npm:2.3.1, punycode@npm:^2.1.0, punycode@npm:^2.1.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 @@ -24783,13 +24771,6 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.1.1": - version: 2.1.1 - resolution: "punycode@npm:2.1.1" - checksum: 10/939daa010c2cacebdb060c40ecb52fef0a739324a66f7fffe0f94353a1ee83e3b455e9032054c4a0c4977b0a28e27086f2171c392832b59a01bd948fd8e20914 - languageName: node - linkType: hard - "pure-rand@npm:^6.0.0": version: 6.0.3 resolution: "pure-rand@npm:6.0.3" From bf3a3834899e5f7eca66bc633eb82ddc48ae43d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:53:46 +0100 Subject: [PATCH 043/122] I18n: Download translations from Crowdin (#89401) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 21 ++++++++++++++++++++- public/locales/es-ES/grafana.json | 21 ++++++++++++++++++++- public/locales/fr-FR/grafana.json | 21 ++++++++++++++++++++- public/locales/pt-BR/grafana.json | 21 ++++++++++++++++++++- public/locales/zh-Hans/grafana.json | 21 ++++++++++++++++++++- 5 files changed, 100 insertions(+), 5 deletions(-) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index b9e7776d417..d31d062d0ac 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -25,6 +25,14 @@ "user": "Nutzer" } }, + "alert-labels": { + "button": { + "hide": "", + "show": { + "tooltip": "" + } + } + }, "alert-rule-form": { "evaluation-behaviour": { "description": { @@ -138,6 +146,16 @@ "text": "" } }, + "central-alert-history": { + "error": "", + "filter": { + "button": { + "clear": "" + }, + "label": "", + "placeholder": "" + } + }, "clipboard-button": { "inline-toast": { "success": "Kopiert" @@ -1166,7 +1184,7 @@ "public": { "title": "Öffentliche Dashboards" }, - "recentlyDeleted": { + "recently-deleted": { "subtitle": "", "title": "" }, @@ -1615,6 +1633,7 @@ "tree": { "collapse": "", "expand": "", + "headline": "", "search": "" } }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 2fd555955b8..5ce48ee6b95 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -25,6 +25,14 @@ "user": "Usuario" } }, + "alert-labels": { + "button": { + "hide": "", + "show": { + "tooltip": "" + } + } + }, "alert-rule-form": { "evaluation-behaviour": { "description": { @@ -138,6 +146,16 @@ "text": "" } }, + "central-alert-history": { + "error": "", + "filter": { + "button": { + "clear": "" + }, + "label": "", + "placeholder": "" + } + }, "clipboard-button": { "inline-toast": { "success": "Copiado" @@ -1166,7 +1184,7 @@ "public": { "title": "Paneles de control públicos" }, - "recentlyDeleted": { + "recently-deleted": { "subtitle": "", "title": "" }, @@ -1615,6 +1633,7 @@ "tree": { "collapse": "", "expand": "", + "headline": "", "search": "" } }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 865cd291607..91dc5716510 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -25,6 +25,14 @@ "user": "Utilisateur" } }, + "alert-labels": { + "button": { + "hide": "", + "show": { + "tooltip": "" + } + } + }, "alert-rule-form": { "evaluation-behaviour": { "description": { @@ -138,6 +146,16 @@ "text": "" } }, + "central-alert-history": { + "error": "", + "filter": { + "button": { + "clear": "" + }, + "label": "", + "placeholder": "" + } + }, "clipboard-button": { "inline-toast": { "success": "Copié" @@ -1166,7 +1184,7 @@ "public": { "title": "Tableaux de bord publics" }, - "recentlyDeleted": { + "recently-deleted": { "subtitle": "", "title": "" }, @@ -1615,6 +1633,7 @@ "tree": { "collapse": "", "expand": "", + "headline": "", "search": "" } }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index ec8e2689a80..484f1128ff9 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -25,6 +25,14 @@ "user": "Usuário" } }, + "alert-labels": { + "button": { + "hide": "", + "show": { + "tooltip": "" + } + } + }, "alert-rule-form": { "evaluation-behaviour": { "description": { @@ -138,6 +146,16 @@ "text": "" } }, + "central-alert-history": { + "error": "", + "filter": { + "button": { + "clear": "" + }, + "label": "", + "placeholder": "" + } + }, "clipboard-button": { "inline-toast": { "success": "Copiado" @@ -1166,7 +1184,7 @@ "public": { "title": "Painéis de controle públicos" }, - "recentlyDeleted": { + "recently-deleted": { "subtitle": "", "title": "" }, @@ -1615,6 +1633,7 @@ "tree": { "collapse": "", "expand": "", + "headline": "", "search": "" } }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 2511dc32c60..4fe0e4d0762 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -25,6 +25,14 @@ "user": "用户" } }, + "alert-labels": { + "button": { + "hide": "", + "show": { + "tooltip": "" + } + } + }, "alert-rule-form": { "evaluation-behaviour": { "description": { @@ -133,6 +141,16 @@ "text": "" } }, + "central-alert-history": { + "error": "", + "filter": { + "button": { + "clear": "" + }, + "label": "", + "placeholder": "" + } + }, "clipboard-button": { "inline-toast": { "success": "已复制" @@ -1160,7 +1178,7 @@ "public": { "title": "公共仪表板" }, - "recentlyDeleted": { + "recently-deleted": { "subtitle": "", "title": "" }, @@ -1608,6 +1626,7 @@ "tree": { "collapse": "", "expand": "", + "headline": "", "search": "" } }, From 44fd13c742e606b8409e23eb62cab8bab24310f1 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 20 Jun 2024 12:56:39 +0200 Subject: [PATCH 044/122] Repurpose wrong datasource UID feature flag (#89363) --- .../feature-toggles/index.md | 2 +- .../src/types/featureToggles.gen.ts | 2 +- pkg/services/datasources/errors.go | 1 + pkg/services/datasources/service/store.go | 8 ++--- .../datasources/service/store_test.go | 17 ++++------- pkg/services/featuremgmt/registry.go | 12 ++++---- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 8 ++--- pkg/services/featuremgmt/toggles_gen.json | 17 +++++++++-- pkg/util/shortid_generator.go | 11 ------- pkg/util/shortid_generator_test.go | 29 ------------------- 11 files changed, 38 insertions(+), 71 deletions(-) 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 fc6e4bd12ad..b2413585105 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -182,7 +182,6 @@ Experimental features might be changed or removed without prior notice. | `accessActionSets` | Introduces action sets for resource permissions | | `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 | -| `autofixDSUID` | Automatically migrates invalid datasource UIDs | | `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | | `newDashboardSharingComponent` | Enables the new sharing drawer design | | `alertingListViewV2` | Enables the new alert list view design | @@ -191,6 +190,7 @@ Experimental features might be changed or removed without prior notice. | `alertingCentralAlertHistory` | Enables the new central alert history. | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | | `pinNavItems` | Enables pinning of nav items | +| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | | `databaseReadReplica` | Use a read replica for some database queries. | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a8ac9a8e1b0..a7b45c16968 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -180,7 +180,6 @@ export interface FeatureToggles { disableNumericMetricsSortingInExpressions?: boolean; grafanaManagedRecordingRules?: boolean; queryLibrary?: boolean; - autofixDSUID?: boolean; logsExploreTableDefaultVisualization?: boolean; newDashboardSharingComponent?: boolean; alertingListViewV2?: boolean; @@ -196,6 +195,7 @@ export interface FeatureToggles { authZGRPCServer?: boolean; openSearchBackendFlowEnabled?: boolean; ssoSettingsLDAP?: boolean; + failWrongDSUID?: boolean; databaseReadReplica?: boolean; zanzana?: boolean; } diff --git a/pkg/services/datasources/errors.go b/pkg/services/datasources/errors.go index e4db5a71d0f..8147b4a4e65 100644 --- a/pkg/services/datasources/errors.go +++ b/pkg/services/datasources/errors.go @@ -18,4 +18,5 @@ var ( ErrDataSourceNameInvalid = errutil.ValidationFailed("datasource.nameInvalid", errutil.WithPublicMessage("Invalid datasource name.")) ErrDataSourceURLInvalid = errutil.ValidationFailed("datasource.urlInvalid", errutil.WithPublicMessage("Invalid datasource url.")) ErrDataSourceAPIVersionInvalid = errutil.ValidationFailed("datasource.apiVersionInvalid", errutil.WithPublicMessage("Invalid datasource apiVersion.")) + ErrDataSourceUIDInvalid = errutil.ValidationFailed("datasource.uidInvalid", errutil.WithPublicMessage("Invalid datasource UID.")) ) diff --git a/pkg/services/datasources/service/store.go b/pkg/services/datasources/service/store.go index 27bbc95935c..601872aefc9 100644 --- a/pkg/services/datasources/service/store.go +++ b/pkg/services/datasources/service/store.go @@ -252,8 +252,8 @@ func (ss *SqlStore) AddDataSource(ctx context.Context, cmd *datasources.AddDataS cmd.UID = uid } else if err := util.ValidateUID(cmd.UID); err != nil { logDeprecatedInvalidDsUid(ss.logger, cmd.UID, cmd.Name, "create", err) - if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagAutofixDSUID) { - return fmt.Errorf("invalid UID for datasource %s: %w", cmd.Name, err) + if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagFailWrongDSUID) { + return datasources.ErrDataSourceUIDInvalid.Errorf("invalid UID for datasource %s: %w", cmd.Name, err) } } @@ -329,8 +329,8 @@ func (ss *SqlStore) UpdateDataSource(ctx context.Context, cmd *datasources.Updat if cmd.UID != "" { if err := util.ValidateUID(cmd.UID); err != nil { logDeprecatedInvalidDsUid(ss.logger, cmd.UID, cmd.Name, "update", err) - if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagAutofixDSUID) { - cmd.UID = util.AutofixUID(cmd.UID) + if ss.features != nil && ss.features.IsEnabled(ctx, featuremgmt.FlagFailWrongDSUID) { + return datasources.ErrDataSourceUIDInvalid.Errorf("invalid UID for datasource %s: %w", cmd.Name, err) } } } diff --git a/pkg/services/datasources/service/store_test.go b/pkg/services/datasources/service/store_test.go index e0cd2e06dc2..5ade7093422 100644 --- a/pkg/services/datasources/service/store_test.go +++ b/pkg/services/datasources/service/store_test.go @@ -104,7 +104,7 @@ func TestIntegrationDataAccess(t *testing.T) { ss := SqlStore{ db: db, logger: log.NewNopLogger(), - features: featuremgmt.WithFeatures(featuremgmt.FlagAutofixDSUID), + features: featuremgmt.WithFeatures(featuremgmt.FlagFailWrongDSUID), } cmd := defaultAddDatasourceCommand cmd.UID = "test/uid" @@ -232,28 +232,21 @@ func TestIntegrationDataAccess(t *testing.T) { require.NoError(t, err) }) - t.Run("updates UID with a valid one", func(t *testing.T) { + t.Run("fails to update a datasource with an invalid uid", func(t *testing.T) { db := db.InitTestDB(t) ds := initDatasource(db) ss := SqlStore{ db: db, logger: log.NewNopLogger(), - features: featuremgmt.WithFeatures(featuremgmt.FlagAutofixDSUID), + features: featuremgmt.WithFeatures(featuremgmt.FlagFailWrongDSUID), } require.NotEmpty(t, ds.UID) cmd := defaultUpdateDatasourceCommand cmd.ID = ds.ID cmd.UID = "new/uid" - res, err := ss.UpdateDataSource(context.Background(), &cmd) - require.NoError(t, err) - require.Equal(t, "new-uid", res.UID) - - // Return the datasource with the valid UID - query := datasources.GetDataSourceQuery{UID: "new-uid", OrgID: 10} - dataSource, err := ss.GetDataSource(context.Background(), &query) - require.NoError(t, err) - require.Equal(t, "new-uid", dataSource.UID) + _, err := ss.UpdateDataSource(context.Background(), &cmd) + require.ErrorContains(t, err, "invalid format of UID") }) }) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b769382e364..9117d55c666 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1222,12 +1222,6 @@ var ( FrontendOnly: false, AllowSelfServe: false, }, - { - Name: "autofixDSUID", - Description: "Automatically migrates invalid datasource UIDs", - Stage: FeatureStageExperimental, - Owner: grafanaPluginsPlatformSquad, - }, { Name: "logsExploreTableDefaultVisualization", Description: "Sets the logs table as default visualisation in logs explore", @@ -1336,6 +1330,12 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, + { + Name: "failWrongDSUID", + Description: "Throws an error if a datasource has an invalid UIDs", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + }, { Name: "databaseReadReplica", Description: "Use a read replica for some database queries.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 020c6443d5b..da88637ca63 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -161,7 +161,6 @@ accessActionSets,experimental,@grafana/identity-access-team,false,false,false disableNumericMetricsSortingInExpressions,experimental,@grafana/observability-metrics,false,true,false grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false queryLibrary,experimental,@grafana/explore-squad,false,false,false -autofixDSUID,experimental,@grafana/plugins-platform-backend,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true newDashboardSharingComponent,experimental,@grafana/sharing-squad,false,false,true alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true @@ -177,5 +176,6 @@ pinNavItems,experimental,@grafana/grafana-frontend-platform,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false openSearchBackendFlowEnabled,preview,@grafana/aws-datasources,false,false,false ssoSettingsLDAP,experimental,@grafana/identity-access-team,false,false,false +failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false databaseReadReplica,experimental,@grafana/grafana-backend-services-squad,false,false,false zanzana,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 0a4314ba04e..b56aa2c1e46 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -655,10 +655,6 @@ const ( // Enables Query Library feature in Explore FlagQueryLibrary = "queryLibrary" - // FlagAutofixDSUID - // Automatically migrates invalid datasource UIDs - FlagAutofixDSUID = "autofixDSUID" - // FlagLogsExploreTableDefaultVisualization // Sets the logs table as default visualisation in logs explore FlagLogsExploreTableDefaultVisualization = "logsExploreTableDefaultVisualization" @@ -719,6 +715,10 @@ const ( // Use the new SSO Settings API to configure LDAP FlagSsoSettingsLDAP = "ssoSettingsLDAP" + // FlagFailWrongDSUID + // Throws an error if a datasource has an invalid UIDs + FlagFailWrongDSUID = "failWrongDSUID" + // FlagDatabaseReadReplica // Use a read replica for some database queries. FlagDatabaseReadReplica = "databaseReadReplica" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 92a0a954eb4..e26f18d188b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -401,8 +401,9 @@ { "metadata": { "name": "autofixDSUID", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-05-03T11:32:07Z" + "resourceVersion": "1717578796182", + "creationTimestamp": "2024-05-03T11:32:07Z", + "deletionTimestamp": "2024-06-18T14:28:32Z" }, "spec": { "description": "Automatically migrates invalid datasource UIDs", @@ -915,6 +916,18 @@ "frontend": true } }, + { + "metadata": { + "name": "failWrongDSUID", + "resourceVersion": "1718721033692", + "creationTimestamp": "2024-06-18T14:30:33Z" + }, + "spec": { + "description": "Throws an error if a datasource has an invalid UIDs", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, { "metadata": { "name": "faroDatasourceSelector", diff --git a/pkg/util/shortid_generator.go b/pkg/util/shortid_generator.go index b56aa89b633..152553f1824 100644 --- a/pkg/util/shortid_generator.go +++ b/pkg/util/shortid_generator.go @@ -32,7 +32,6 @@ var mtx sync.Mutex // Legacy UID pattern var validUIDCharPattern = `a-zA-Z0-9\-\_` var validUIDPattern = regexp.MustCompile(`^[` + validUIDCharPattern + `]*$`).MatchString -var validUIDReplacer = regexp.MustCompile(`[^` + validUIDCharPattern + `]`).ReplaceAllString // IsValidShortUID checks if short unique identifier contains valid characters // NOTE: future Grafana UIDs will need conform to https://github.com/kubernetes/apimachinery/blob/master/pkg/util/validation/validation.go#L43 @@ -93,13 +92,3 @@ func ValidateUID(uid string) error { } return nil } - -func AutofixUID(uid string) string { - if IsShortUIDTooLong(uid) { - return uid[:MaxUIDLength] - } - if !IsValidShortUID(uid) { - uid = validUIDReplacer(uid, "-") - } - return uid -} diff --git a/pkg/util/shortid_generator_test.go b/pkg/util/shortid_generator_test.go index ea273522889..6bc4bb05b5c 100644 --- a/pkg/util/shortid_generator_test.go +++ b/pkg/util/shortid_generator_test.go @@ -143,32 +143,3 @@ func TestValidateUID(t *testing.T) { }) } } - -func TestAutofixUID(t *testing.T) { - var tests = []struct { - name string - uid string - expected string - }{ - { - name: "return input when input is valid", - uid: "f8cc010c-ee72-4681-89d2-d46e1bd47d33", - expected: "f8cc010c-ee72-4681-89d2-d46e1bd47d33", - }, - { - name: "generate new uid when input is too long", - uid: strings.Repeat("1", MaxUIDLength+1), - expected: strings.Repeat("1", MaxUIDLength), - }, - { - name: "generate new uid when input has invalid characters", - uid: "f8cc010c.ee72.4681;89d2+d46e1bd47d33", - expected: "f8cc010c-ee72-4681-89d2-d46e1bd47d33", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, AutofixUID(tt.uid)) - }) - } -} From 78c8a26906957f915e8b0df7018c7aeccf455c5d Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Thu, 20 Jun 2024 13:51:51 +0200 Subject: [PATCH 045/122] Chore: Adding build number to plugin schema (#89464) adding build number --- docs/sources/developers/plugins/plugin.schema.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 3dfd8a17358..647a3fe485d 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -72,6 +72,10 @@ "pr": { "type": "number", "description": "GitHub pull request the plugin was built from" + }, + "build": { + "type": "number", + "description": "Build job number used to build this plugin." } } }, From 2a714601a71883a51314b8329f5291f72e3059b3 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 20 Jun 2024 15:04:22 +0200 Subject: [PATCH 046/122] Chore: Remove prometheusIncrementalQueryInstrumentation feature toggle (#89463) * remove prometheusIncrementalQueryInstrumentation feature toggle * remove profile method * remove more about profiling --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - packages/grafana-prometheus/src/datasource.ts | 9 - .../src/querycache/QueryCache.test.ts | 15 +- .../src/querycache/QueryCache.ts | 187 ------------------ pkg/services/featuremgmt/registry.go | 7 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 3 +- 9 files changed, 3 insertions(+), 225 deletions(-) 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 b2413585105..5f6237ff724 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -138,7 +138,6 @@ Experimental features might be changed or removed without prior notice. | `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | | `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | | `vizAndWidgetSplit` | Split panels between visualizations and widgets | -| `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | | `mlExpressions` | Enable support for Machine Learning in server-side expressions | | `metricsSummary` | Enables metrics summary queries in the Tempo data source | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a7b45c16968..8af56fc11c3 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -82,7 +82,6 @@ export interface FeatureToggles { sqlDatasourceDatabaseSelection?: boolean; recordedQueriesMulti?: boolean; vizAndWidgetSplit?: boolean; - prometheusIncrementalQueryInstrumentation?: boolean; logsExploreTableVisualisation?: boolean; awsDatasourcesTempCredentials?: boolean; transformationsRedesign?: boolean; diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 34688f47465..3296a8da674 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -139,7 +139,6 @@ export class PrometheusDatasource this.cache = new QueryCache({ getTargetSignature: this.getPrometheusTargetSignature.bind(this), overlapString: instanceSettings.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow, - profileFunction: this.getPrometheusProfileData.bind(this), }); // This needs to be here and cannot be static because of how annotations typing affects casting of data source @@ -162,14 +161,6 @@ export class PrometheusDatasource return query.expr; } - getPrometheusProfileData(request: DataQueryRequest, targ: PromQuery) { - return { - interval: targ.interval ?? request.interval, - expr: this.interpolateString(targ.expr), - datasource: 'Prometheus', - }; - } - /** * Get target signature for query caching * @param request diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts index 78ff585ec20..9efe6a73288 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts @@ -6,7 +6,7 @@ import { DataFrame, DataQueryRequest, DateTime, dateTime, TimeRange } from '@gra import { QueryEditorMode } from '../querybuilder/shared/types'; import { PromQuery } from '../types'; -import { DatasourceProfileData, QueryCache } from './QueryCache'; +import { QueryCache } from './QueryCache'; import { IncrementalStorageDataFrameScenarios } from './QueryCacheTestData'; // Will not interpolate vars! @@ -60,14 +60,6 @@ const mockPromRequest = (request?: Partial>): DataQu }; }; -const getPromProfileData = (request: DataQueryRequest, targ: PromQuery): DatasourceProfileData => { - return { - expr: targ.expr, - interval: targ.interval ?? request.interval, - datasource: 'prom', - }; -}; - describe('QueryCache: Generic', function () { it('instantiates', () => { const storage = new QueryCache({ @@ -192,7 +184,6 @@ describe('QueryCache: Prometheus', function () { const storage = new QueryCache({ getTargetSignature: getPrometheusTargetSignature, overlapString: '10m', - profileFunction: getPromProfileData, }); const firstFrames = scenario.first.dataFrames as unknown as DataFrame[]; const secondFrames = scenario.second.dataFrames as unknown as DataFrame[]; @@ -328,7 +319,6 @@ describe('QueryCache: Prometheus', function () { const storage = new QueryCache({ getTargetSignature: getPrometheusTargetSignature, overlapString: '10m', - profileFunction: getPromProfileData, }); // Initial request with all data for time range @@ -489,7 +479,6 @@ describe('QueryCache: Prometheus', function () { const storage = new QueryCache({ getTargetSignature: getPrometheusTargetSignature, overlapString: '10m', - profileFunction: getPromProfileData, }); const cacheRequest = storage.requestInfo(request); expect(cacheRequest.requests[0]).toBe(request); @@ -501,7 +490,6 @@ describe('QueryCache: Prometheus', function () { const storage = new QueryCache({ getTargetSignature: getPrometheusTargetSignature, overlapString: '10m', - profileFunction: getPromProfileData, }); const cacheRequest = storage.requestInfo(request); expect(cacheRequest.requests[0]).toBe(request); @@ -513,7 +501,6 @@ describe('QueryCache: Prometheus', function () { const storage = new QueryCache({ getTargetSignature: getPrometheusTargetSignature, overlapString: '10m', - profileFunction: getPromProfileData, }); const cacheRequest = storage.requestInfo(request); expect(cacheRequest.requests[0]).toBe(request); diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.ts b/packages/grafana-prometheus/src/querycache/QueryCache.ts index 05ad2c3f1d3..f4b955b82a6 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.ts @@ -9,8 +9,6 @@ import { isValidDuration, parseDuration, } from '@grafana/data'; -import { faro } from '@grafana/faro-web-sdk'; -import { config, reportInteraction } from '@grafana/runtime'; import { amendTable, Table, trimTable } from '../gcopypaste/app/features/live/data/amendTimeSeries'; import { PromQuery } from '../types'; @@ -19,8 +17,6 @@ import { PromQuery } from '../types'; // (must be stable across query changes, time range changes / interval changes / panel resizes / template variable changes) type TargetIdent = string; -type RequestID = string; - // query + template variables + interval + raw time range // used for full target cache busting -> full range re-query type TargetSig = string; @@ -44,22 +40,6 @@ export interface CacheRequestInfo { shouldCache: boolean; } -export interface DatasourceProfileData { - interval?: string; - expr: string; - datasource: string; -} - -interface ProfileData extends DatasourceProfileData { - identity: string; - bytes: number | null; - dashboardUID: string; - panelId?: number; - from: string; - queryRangeSeconds: number; - refreshIntervalMs: number; -} - /** * Get field identity * This is the string used to uniquely identify a field within a "target" @@ -76,40 +56,12 @@ export const getFieldIdent = (field: Field) => `${field.type}|${field.name}|${JS export class QueryCache { private overlapWindowMs: number; private getTargetSignature: (request: DataQueryRequest, target: T) => string; - private getProfileData?: (request: DataQueryRequest, target: T) => DatasourceProfileData; - - private perfObeserver?: PerformanceObserver; - private shouldProfile: boolean; - - // send profile events every 10 minutes - sendEventsInterval = 60000 * 10; - - pendingRequestIdsToTargSigs = new Map(); - - pendingAccumulatedEvents = new Map< - string, - { - requestCount: number; - savedBytesTotal: number; - initialRequestSize: number; - lastRequestSize: number; - panelId: string; - dashId: string; - expr: string; - refreshIntervalMs: number; - sent: boolean; - datasource: string; - from: string; - queryRangeSeconds: number; - } - >(); cache = new Map(); constructor(options: { getTargetSignature: (request: DataQueryRequest, target: T) => string; overlapString: string; - profileFunction?: (request: DataQueryRequest, target: T) => DatasourceProfileData; }) { const unverifiedOverlap = options.overlapString; if (isValidDuration(unverifiedOverlap)) { @@ -120,132 +72,9 @@ export class QueryCache { this.overlapWindowMs = durationToMilliseconds(duration); } - if ( - (config.grafanaJavascriptAgent.enabled || config.featureToggles?.prometheusIncrementalQueryInstrumentation) && - options.profileFunction !== undefined - ) { - this.profile(); - this.shouldProfile = true; - } else { - this.shouldProfile = false; - } - this.getProfileData = options.profileFunction; this.getTargetSignature = options.getTargetSignature; } - private profile() { - // Check if PerformanceObserver is supported, and if we have Faro enabled for internal profiling - if (typeof PerformanceObserver === 'function') { - this.perfObeserver = new PerformanceObserver((list: PerformanceObserverEntryList) => { - list.getEntries().forEach((entry) => { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const entryTypeCast: PerformanceResourceTiming = entry as PerformanceResourceTiming; - - // Safari support for this is coming in 16.4: - // https://caniuse.com/mdn-api_performanceresourcetiming_transfersize - // Gating that this exists to prevent runtime errors - const isSupported = typeof entryTypeCast?.transferSize === 'number'; - - if (entryTypeCast?.initiatorType === 'fetch' && isSupported) { - let fetchUrl = entryTypeCast.name; - - if (fetchUrl.includes('/api/ds/query')) { - let match = fetchUrl.match(/requestId=([a-z\d]+)/i); - - if (match) { - let requestId = match[1]; - - const requestTransferSize = Math.round(entryTypeCast.transferSize); - const currentRequest = this.pendingRequestIdsToTargSigs.get(requestId); - - if (currentRequest) { - const entries = this.pendingRequestIdsToTargSigs.entries(); - - for (let [, value] of entries) { - if (value.identity === currentRequest.identity && value.bytes !== null) { - const previous = this.pendingAccumulatedEvents.get(value.identity); - - const savedBytes = value.bytes - requestTransferSize; - - this.pendingAccumulatedEvents.set(value.identity, { - datasource: value.datasource ?? 'N/A', - requestCount: (previous?.requestCount ?? 0) + 1, - savedBytesTotal: (previous?.savedBytesTotal ?? 0) + savedBytes, - initialRequestSize: value.bytes, - lastRequestSize: requestTransferSize, - panelId: currentRequest.panelId?.toString() ?? '', - dashId: currentRequest.dashboardUID ?? '', - expr: currentRequest.expr ?? '', - refreshIntervalMs: currentRequest.refreshIntervalMs ?? 0, - sent: false, - from: currentRequest.from ?? '', - queryRangeSeconds: currentRequest.queryRangeSeconds ?? 0, - }); - - // We don't need to save each subsequent request, only the first one - this.pendingRequestIdsToTargSigs.delete(requestId); - - return; - } - } - - // If we didn't return above, this should be the first request, let's save the observed size - this.pendingRequestIdsToTargSigs.set(requestId, { ...currentRequest, bytes: requestTransferSize }); - } - } - } - } - }); - }); - - this.perfObeserver.observe({ type: 'resource', buffered: false }); - - setInterval(this.sendPendingTrackingEvents, this.sendEventsInterval); - - // Send any pending profile information when the user navigates away - window.addEventListener('beforeunload', this.sendPendingTrackingEvents); - } - } - - sendPendingTrackingEvents = () => { - const entries = this.pendingAccumulatedEvents.entries(); - - for (let [key, value] of entries) { - if (!value.sent) { - const event = { - datasource: value.datasource.toString(), - requestCount: value.requestCount.toString(), - savedBytesTotal: value.savedBytesTotal.toString(), - initialRequestSize: value.initialRequestSize.toString(), - lastRequestSize: value.lastRequestSize.toString(), - panelId: value.panelId.toString(), - dashId: value.dashId.toString(), - expr: value.expr.toString(), - refreshIntervalMs: value.refreshIntervalMs.toString(), - from: value.from.toString(), - queryRangeSeconds: value.queryRangeSeconds.toString(), - }; - - if (config.featureToggles.prometheusIncrementalQueryInstrumentation) { - reportInteraction('grafana_incremental_queries_profile', event); - } else if (faro.api.pushEvent) { - faro.api.pushEvent('incremental query response size', event, 'no-interaction', { - skipDedupe: true, - }); - } - - this.pendingAccumulatedEvents.set(key, { - ...value, - sent: true, - requestCount: 0, - savedBytesTotal: 0, - initialRequestSize: 0, - lastRequestSize: 0, - }); - } - } - }; - // can be used to change full range request to partial, split into multiple requests requestInfo(request: DataQueryRequest): CacheRequestInfo { // TODO: align from/to to interval to increase probability of hitting backend cache @@ -260,27 +89,11 @@ export class QueryCache { let doPartialQuery = shouldCache; let prevTo: TimestampMs | undefined = undefined; - const refreshIntervalMs = request.intervalMs; - // pre-compute reqTargSigs const reqTargSigs = new Map(); request.targets.forEach((targ) => { let targIdent = `${request.dashboardUID}|${request.panelId}|${targ.refId}`; let targSig = this.getTargetSignature(request, targ); // ${request.maxDataPoints} ? - - if (this.shouldProfile && this.getProfileData) { - this.pendingRequestIdsToTargSigs.set(request.requestId, { - ...this.getProfileData(request, targ), - identity: targIdent + '|' + targSig, - bytes: null, - panelId: request.panelId, - dashboardUID: request.dashboardUID ?? '', - from: request.rangeRaw?.from.toString() ?? '', - queryRangeSeconds: request.range.to.diff(request.range.from, 'seconds') ?? '', - refreshIntervalMs: refreshIntervalMs ?? 0, - }); - } - reqTargSigs.set(targIdent, targSig); }); diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9117d55c666..3d763445ea2 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -480,13 +480,6 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, - { - Name: "prometheusIncrementalQueryInstrumentation", - Description: "Adds RudderStack events to incremental queries", - FrontendOnly: true, - Stage: FeatureStageExperimental, - Owner: grafanaObservabilityMetricsSquad, - }, { Name: "logsExploreTableVisualisation", Description: "A table visualisation for logs in Explore", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index da88637ca63..c0cf306fe54 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -63,7 +63,6 @@ frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false, sqlDatasourceDatabaseSelection,preview,@grafana/dataviz-squad,false,false,true recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,true -prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,true logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false transformationsRedesign,GA,@grafana/observability-metrics,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b56aa2c1e46..01c91f4b044 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -263,10 +263,6 @@ const ( // Split panels between visualizations and widgets FlagVizAndWidgetSplit = "vizAndWidgetSplit" - // FlagPrometheusIncrementalQueryInstrumentation - // Adds RudderStack events to incremental queries - FlagPrometheusIncrementalQueryInstrumentation = "prometheusIncrementalQueryInstrumentation" - // FlagLogsExploreTableVisualisation // A table visualisation for logs in Explore FlagLogsExploreTableVisualisation = "logsExploreTableVisualisation" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e26f18d188b..4650fac0999 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1832,7 +1832,8 @@ "metadata": { "name": "prometheusIncrementalQueryInstrumentation", "resourceVersion": "1718727528075", - "creationTimestamp": "2023-07-05T19:39:49Z" + "creationTimestamp": "2023-07-05T19:39:49Z", + "deletionTimestamp": "2024-06-20T11:30:37Z" }, "spec": { "description": "Adds RudderStack events to incremental queries", From b7df121294e10dd6a4ca84f98470acbf0f4048a7 Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 20 Jun 2024 08:04:44 -0500 Subject: [PATCH 047/122] Explore: Change Logs.tsx to a functional component (#87808) * First pass * WIP * why is my cpu angy * Avoid triggering onHiddenSeriesChanged too often onHiddenSeriesChanged should be called only when dataWithConfig changes, not when the callback changes itself. This is actually causing an infinite loop because onHiddenSeriesChanged may not be memoized in the parent and passed as a new callback function on each render. * fix tests * Remove store mock and clear store between tests instead * Fix filtering * First pass adding callbacks and dependencies * Add useRef hook for toggleLegend and topLogsRef * Remove unwanted reactivity register(...) changes outlineItems. A function that calls register(...) behaves like a setter for the state - it cannot react to the state itself. * Separate unmount clean-ups * Ensure unmount is not reactive to props * Make memoized functions stats * Wrap functions passed down to components with useCallback * Fix reporting interaction when context is closed * Adjust several variables to use refs * Post-merge fixes * Ensure scrollIntoView is called with a ref to the container * Update docs * Ensure scrollIntoView is updated only if the scroll element changes * Fix the default pinned log tooltip * Small clean up * Remove console.log() --------- Co-authored-by: Piotr Jamroz Co-authored-by: harisrozajac --- .../ContentOutline/ContentOutlineContext.tsx | 30 +- .../features/explore/Graph/ExploreGraph.tsx | 10 +- .../app/features/explore/Logs/Logs.test.tsx | 291 ++-- public/app/features/explore/Logs/Logs.tsx | 1490 ++++++++--------- public/app/features/explore/state/main.ts | 2 +- 5 files changed, 902 insertions(+), 921 deletions(-) diff --git a/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx b/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx index 44b2a989a60..3e91de93e83 100644 --- a/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx +++ b/public/app/features/explore/ContentOutline/ContentOutlineContext.tsx @@ -16,7 +16,10 @@ export interface ContentOutlineContextProps { outlineItems: ContentOutlineItemContextProps[]; register: RegisterFunction; unregister: (id: string) => void; - unregisterAllChildren: (parentId: string, childType: ITEM_TYPES) => void; + unregisterAllChildren: ( + parentIdGetter: (items: ContentOutlineItemContextProps[]) => string | undefined, + childType: ITEM_TYPES + ) => void; updateOutlineItems: (newItems: ContentOutlineItemContextProps[]) => void; updateItem: (id: string, properties: Partial>) => void; } @@ -193,16 +196,23 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }: ); }, []); - const unregisterAllChildren = useCallback((parentId: string, childType: ITEM_TYPES) => { - setOutlineItems((prevItems) => - prevItems.map((item) => { - if (item.id === parentId) { - item.children = item.children?.filter((child) => child.type !== childType); + const unregisterAllChildren = useCallback( + (parentIdGetter: (items: ContentOutlineItemContextProps[]) => string | undefined, childType: ITEM_TYPES) => { + setOutlineItems((prevItems) => { + const parentId = parentIdGetter(prevItems); + if (!parentId) { + return prevItems; } - return item; - }) - ); - }, []); + return prevItems.map((item) => { + if (item.id === parentId) { + item.children = item.children?.filter((child) => child.type !== childType); + } + return item; + }); + }); + }, + [] + ); useEffect(() => { setOutlineItems((prevItems) => { diff --git a/public/app/features/explore/Graph/ExploreGraph.tsx b/public/app/features/explore/Graph/ExploreGraph.tsx index 86eb5475146..cb09a5481fb 100644 --- a/public/app/features/explore/Graph/ExploreGraph.tsx +++ b/public/app/features/explore/Graph/ExploreGraph.tsx @@ -1,5 +1,5 @@ import { identity } from 'lodash'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { usePrevious } from 'react-use'; import { @@ -154,8 +154,10 @@ export function ExploreGraph({ const structureRev = useStructureRev(dataWithConfig); + const onHiddenSeriesChangedRef = useRef(onHiddenSeriesChanged); + useEffect(() => { - if (onHiddenSeriesChanged) { + if (onHiddenSeriesChangedRef.current) { const hiddenFrames: string[] = []; dataWithConfig.forEach((frame) => { const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity); @@ -163,9 +165,9 @@ export function ExploreGraph({ hiddenFrames.push(getFrameDisplayName(frame)); } }); - onHiddenSeriesChanged(hiddenFrames); + onHiddenSeriesChangedRef.current(hiddenFrames); } - }, [dataWithConfig, onHiddenSeriesChanged]); + }, [dataWithConfig]); const panelContext: PanelContext = { eventsScope: 'explore', diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index a8f70e47270..e5f46da4923 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -1,11 +1,11 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { ComponentProps } from 'react'; +import { Provider } from 'react-redux'; import { DataFrame, EventBusSrv, - ExploreLogsPanelState, ExplorePanelsState, LoadingState, LogLevel, @@ -13,25 +13,20 @@ import { standardTransformersRegistry, toUtc, createDataFrame, + ExploreLogsPanelState, } from '@grafana/data'; import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize'; import { config } from '@grafana/runtime'; -import store from 'app/core/store'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; +import { configureStore } from 'app/store/configureStore'; + +import { initialExploreState } from '../state/main'; +import { makeExplorePaneState } from '../state/utils'; import { Logs } from './Logs'; import { visualisationTypeKey } from './utils/logs'; import { getMockElasticFrame, getMockLokiFrame } from './utils/testMocks.test'; -jest.mock('app/core/store', () => { - return { - getBool: jest.fn(), - getObject: jest.fn((_a, b) => b), - get: jest.fn(), - set: jest.fn(), - }; -}); - const reportInteraction = jest.fn(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -45,26 +40,11 @@ jest.mock('app/core/utils/shortLinks', () => ({ createAndCopyShortLink: (url: string) => createAndCopyShortLink(url), })); -jest.mock('app/store/store', () => ({ - getState: jest.fn().mockReturnValue({ - explore: { - panes: { - left: { - datasource: 'id', - queries: [{ refId: 'A', expr: '', queryType: 'range', datasource: { type: 'loki', uid: 'id' } }], - range: { raw: { from: 'now-1h', to: 'now' } }, - }, - }, - }, - }), - dispatch: jest.fn(), -})); - -const changePanelState = jest.fn(); +const fakeChangePanelState = jest.fn().mockReturnValue({ type: 'fakeAction' }); jest.mock('../state/explorePane', () => ({ ...jest.requireActual('../state/explorePane'), changePanelState: (exploreId: string, panel: 'logs', panelState: {} | ExploreLogsPanelState) => { - return changePanelState(exploreId, panel, panelState); + return fakeChangePanelState(exploreId, panel, panelState); }, })); @@ -72,6 +52,7 @@ describe('Logs', () => { let originalHref = window.location.href; beforeEach(() => { + localStorage.clear(); jest.clearAllMocks(); }); @@ -120,6 +101,7 @@ describe('Logs', () => { ]; const testDataFrame = dataFrame ?? getMockLokiFrame(); + return ( { /> ); }; + const setup = (partialProps?: Partial>, dataFrame?: DataFrame, logs?: LogRowModel[]) => { - return render(getComponent(partialProps, dataFrame ? dataFrame : getMockLokiFrame(), logs)); + const fakeStore = configureStore({ + explore: { + ...initialExploreState, + panes: { + left: makeExplorePaneState(), + }, + }, + }); + + const { rerender } = render( + + {getComponent(partialProps, dataFrame ? dataFrame : getMockLokiFrame(), logs)} + + ); + return { rerender, store: fakeStore }; }; describe('scrolling behavior', () => { @@ -216,40 +213,47 @@ describe('Logs', () => { it('should render a load more button', () => { const scanningStarted = jest.fn(); + const store = configureStore({ + explore: { + ...initialExploreState, + }, + }); render( - undefined} - logsVolumeEnabled={true} - onSetLogsVolumeEnabled={() => null} - onClickFilterLabel={() => null} - onClickFilterOutLabel={() => null} - logsVolumeData={undefined} - loadLogsVolumeData={() => undefined} - logRows={[]} - onStartScanning={scanningStarted} - timeZone={'utc'} - width={50} - loading={false} - loadingState={LoadingState.Done} - absoluteRange={{ - from: toUtc('2019-01-01 10:00:00').valueOf(), - to: toUtc('2019-01-01 16:00:00').valueOf(), - }} - range={{ - from: toUtc('2019-01-01 10:00:00'), - to: toUtc('2019-01-01 16:00:00'), - raw: { from: 'now-1h', to: 'now' }, - }} - addResultsToCache={() => {}} - onChangeTime={() => {}} - clearCache={() => {}} - getFieldLinks={() => { - return []; - }} - eventBus={new EventBusSrv()} - isFilterLabelActive={jest.fn()} - /> + + undefined} + logsVolumeEnabled={true} + onSetLogsVolumeEnabled={() => null} + onClickFilterLabel={() => null} + onClickFilterOutLabel={() => null} + logsVolumeData={undefined} + loadLogsVolumeData={() => undefined} + logRows={[]} + onStartScanning={scanningStarted} + timeZone={'utc'} + width={50} + loading={false} + loadingState={LoadingState.Done} + absoluteRange={{ + from: toUtc('2019-01-01 10:00:00').valueOf(), + to: toUtc('2019-01-01 16:00:00').valueOf(), + }} + range={{ + from: toUtc('2019-01-01 10:00:00'), + to: toUtc('2019-01-01 16:00:00'), + raw: { from: 'now-1h', to: 'now' }, + }} + addResultsToCache={() => {}} + onChangeTime={() => {}} + clearCache={() => {}} + getFieldLinks={() => { + return []; + }} + eventBus={new EventBusSrv()} + isFilterLabelActive={jest.fn()} + /> + ); const button = screen.getByRole('button', { name: /scan for older logs/i, @@ -259,40 +263,47 @@ describe('Logs', () => { }); it('should render a stop scanning button', () => { + const store = configureStore({ + explore: { + ...initialExploreState, + }, + }); render( - undefined} - logsVolumeEnabled={true} - onSetLogsVolumeEnabled={() => null} - onClickFilterLabel={() => null} - onClickFilterOutLabel={() => null} - logsVolumeData={undefined} - loadLogsVolumeData={() => undefined} - logRows={[]} - scanning={true} - timeZone={'utc'} - width={50} - loading={false} - loadingState={LoadingState.Done} - absoluteRange={{ - from: toUtc('2019-01-01 10:00:00').valueOf(), - to: toUtc('2019-01-01 16:00:00').valueOf(), - }} - range={{ - from: toUtc('2019-01-01 10:00:00'), - to: toUtc('2019-01-01 16:00:00'), - raw: { from: 'now-1h', to: 'now' }, - }} - addResultsToCache={() => {}} - onChangeTime={() => {}} - clearCache={() => {}} - getFieldLinks={() => { - return []; - }} - eventBus={new EventBusSrv()} - isFilterLabelActive={jest.fn()} - /> + + undefined} + logsVolumeEnabled={true} + onSetLogsVolumeEnabled={() => null} + onClickFilterLabel={() => null} + onClickFilterOutLabel={() => null} + logsVolumeData={undefined} + loadLogsVolumeData={() => undefined} + logRows={[]} + scanning={true} + timeZone={'utc'} + width={50} + loading={false} + loadingState={LoadingState.Done} + absoluteRange={{ + from: toUtc('2019-01-01 10:00:00').valueOf(), + to: toUtc('2019-01-01 16:00:00').valueOf(), + }} + range={{ + from: toUtc('2019-01-01 10:00:00'), + to: toUtc('2019-01-01 16:00:00'), + raw: { from: 'now-1h', to: 'now' }, + }} + addResultsToCache={() => {}} + onChangeTime={() => {}} + clearCache={() => {}} + getFieldLinks={() => { + return []; + }} + eventBus={new EventBusSrv()} + isFilterLabelActive={jest.fn()} + /> + ); expect( @@ -304,42 +315,48 @@ describe('Logs', () => { it('should render a stop scanning button', () => { const scanningStopped = jest.fn(); - + const store = configureStore({ + explore: { + ...initialExploreState, + }, + }); render( - undefined} - logsVolumeEnabled={true} - onSetLogsVolumeEnabled={() => null} - onClickFilterLabel={() => null} - onClickFilterOutLabel={() => null} - logsVolumeData={undefined} - loadLogsVolumeData={() => undefined} - logRows={[]} - scanning={true} - onStopScanning={scanningStopped} - timeZone={'utc'} - width={50} - loading={false} - loadingState={LoadingState.Done} - absoluteRange={{ - from: toUtc('2019-01-01 10:00:00').valueOf(), - to: toUtc('2019-01-01 16:00:00').valueOf(), - }} - range={{ - from: toUtc('2019-01-01 10:00:00'), - to: toUtc('2019-01-01 16:00:00'), - raw: { from: 'now-1h', to: 'now' }, - }} - addResultsToCache={() => {}} - onChangeTime={() => {}} - clearCache={() => {}} - getFieldLinks={() => { - return []; - }} - eventBus={new EventBusSrv()} - isFilterLabelActive={jest.fn()} - /> + + undefined} + logsVolumeEnabled={true} + onSetLogsVolumeEnabled={() => null} + onClickFilterLabel={() => null} + onClickFilterOutLabel={() => null} + logsVolumeData={undefined} + loadLogsVolumeData={() => undefined} + logRows={[]} + scanning={true} + onStopScanning={scanningStopped} + timeZone={'utc'} + width={50} + loading={false} + loadingState={LoadingState.Done} + absoluteRange={{ + from: toUtc('2019-01-01 10:00:00').valueOf(), + to: toUtc('2019-01-01 16:00:00').valueOf(), + }} + range={{ + from: toUtc('2019-01-01 10:00:00'), + to: toUtc('2019-01-01 16:00:00'), + raw: { from: 'now-1h', to: 'now' }, + }} + addResultsToCache={() => {}} + onChangeTime={() => {}} + clearCache={() => {}} + getFieldLinks={() => { + return []; + }} + eventBus={new EventBusSrv()} + isFilterLabelActive={jest.fn()} + /> + ); const button = screen.getByRole('button', { @@ -363,12 +380,12 @@ describe('Logs', () => { describe('for permalinking', () => { it('should dispatch a `changePanelState` event without the id', () => { const panelState = { logs: { id: '1' } }; - const { rerender } = setup({ loading: false, panelState }); + const { rerender, store } = setup({ loading: false, panelState }); - rerender(getComponent({ loading: true, exploreId: 'right', panelState })); - rerender(getComponent({ loading: false, exploreId: 'right', panelState })); + rerender({getComponent({ loading: true, exploreId: 'right', panelState })}); + rerender({getComponent({ loading: false, exploreId: 'right', panelState })}); - expect(changePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} }); + expect(fakeChangePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} }); }); it('should scroll the scrollElement into view if rows contain id', () => { @@ -491,23 +508,17 @@ describe('Logs', () => { }); it('should use default state from localstorage - table', async () => { - const oldGet = store.get; - store.get = jest.fn().mockReturnValue('table'); localStorage.setItem(visualisationTypeKey, 'table'); setup({}); const table = await screen.findByTestId('logRowsTable'); expect(table).toBeInTheDocument(); - store.get = oldGet; }); it('should use default state from localstorage - logs', async () => { - const oldGet = store.get; - store.get = jest.fn().mockReturnValue('logs'); localStorage.setItem(visualisationTypeKey, 'logs'); setup({}); const table = await screen.findByTestId('logRows'); expect(table).toBeInTheDocument(); - store.get = oldGet; }); it('should change visualisation to table on toggle (elastic)', async () => { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index a68e8e79e35..fb671e75b54 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1,38 +1,41 @@ import { css, cx } from '@emotion/css'; import { capitalize, groupBy } from 'lodash'; import memoizeOne from 'memoize-one'; -import React, { createRef, PureComponent } from 'react'; +import React, { useCallback, useEffect, useState, useRef } from 'react'; +import { usePrevious, useUnmount } from 'react-use'; import { - AbsoluteTimeRange, - CoreApp, + SplitOpen, + LogRowModel, + LogsMetaItem, DataFrame, - DataHoverClearEvent, - DataHoverEvent, + DataQuery, + AbsoluteTimeRange, + GrafanaTheme2, + LoadingState, + TimeZone, + RawTimeRange, DataQueryResponse, + LogRowContextOptions, + LinkModel, EventBus, - ExploreLogsPanelState, ExplorePanelsState, Field, - GrafanaTheme2, - LinkModel, - LoadingState, - LogLevel, - LogRowContextOptions, - LogRowModel, - LogsDedupDescription, - LogsDedupStrategy, - LogsMetaItem, - LogsSortOrder, - rangeUtil, - RawTimeRange, - serializeStateToUrlParam, - SplitOpen, TimeRange, + LogsDedupStrategy, + LogsSortOrder, + LogLevel, + DataTopic, + CoreApp, + LogsDedupDescription, + rangeUtil, + ExploreLogsPanelState, + DataHoverClearEvent, + DataHoverEvent, + serializeStateToUrlParam, urlUtil, } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { DataQuery, DataTopic, TimeZone } from '@grafana/schema'; import { Button, InlineField, @@ -50,21 +53,21 @@ import { Trans } from 'app/core/internationalization'; import store from 'app/core/store'; import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll'; +import { LogRows } from 'app/features/logs/components/LogRows'; +import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal'; +import { LogLevelColor, dedupLogRows, filterLogLevels } from 'app/features/logs/logsModel'; import { getLogLevel, getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils'; -import { dispatch, getState } from 'app/store/store'; +import { getState } from 'app/store/store'; +import { ExploreItemState, useDispatch } from 'app/types'; -import { ExploreItemState } from '../../../types'; -import { LogRows } from '../../logs/components/LogRows'; -import { LogRowContextModal } from '../../logs/components/log-context/LogRowContextModal'; -import { dedupLogRows, filterLogLevels, LogLevelColor } from '../../logs/logsModel'; -import { ContentOutlineContext } from '../ContentOutline/ContentOutlineContext'; +import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext'; import { getUrlStateFromPaneState } from '../hooks/useStateSync'; import { changePanelState } from '../state/explorePane'; import { LogsFeedback } from './LogsFeedback'; import { LogsMetaRow } from './LogsMetaRow'; import LogsNavigation from './LogsNavigation'; -import { getLogsTableHeight, LogsTableWrap } from './LogsTableWrap'; +import { LogsTableWrap, getLogsTableHeight } from './LogsTableWrap'; import { LogsVolumePanelList } from './LogsVolumePanelList'; import { SETTINGS_KEYS, visualisationTypeKey } from './utils/logs'; @@ -119,25 +122,6 @@ interface Props extends Themeable2 { export type LogsVisualisationType = 'table' | 'logs'; -interface State { - showLabels: boolean; - showTime: boolean; - wrapLogMessage: boolean; - prettifyLogMessage: boolean; - dedupStrategy: LogsDedupStrategy; - hiddenLogLevels: LogLevel[]; - logsSortOrder: LogsSortOrder; - isFlipping: boolean; - displayedFields: string[]; - forceEscape: boolean; - contextOpen: boolean; - contextRow?: LogRowModel; - tableFrame?: DataFrame; - visualisationType?: LogsVisualisationType; - logsContainer?: HTMLDivElement; - pinLineButtonTooltipTitle?: PopoverContent; -} - // we need to define the order of these explicitly const DEDUP_OPTIONS = [ LogsDedupStrategy.none, @@ -162,82 +146,90 @@ const getDefaultVisualisationType = (): LogsVisualisationType => { const PINNED_LOGS_LIMIT = 3; -class UnthemedLogs extends PureComponent { - flipOrderTimer?: number; - cancelFlippingTimer?: number; - topLogsRef = createRef(); - logsVolumeEventBus: EventBus; - static contextType = ContentOutlineContext; - declare context: React.ContextType; - // @ts-ignore - private toggleLegendRef: React.MutableRefObject<(name: string, mode: SeriesVisibilityChangeMode) => void> = - React.createRef(); +const UnthemedLogs: React.FunctionComponent = (props: Props) => { + const { + width, + splitOpen, + logRows, + logsMeta, + logsVolumeEnabled, + logsVolumeData, + loadLogsVolumeData, + loading = false, + onClickFilterLabel, + onClickFilterOutLabel, + timeZone, + scanning, + scanRange, + showContextToggle, + absoluteRange, + onChangeTime, + getFieldLinks, + theme, + logsQueries, + clearCache, + addResultsToCache, + exploreId, + getRowContext, + getLogRowContextUi, + getRowContextQuery, + loadMoreLogs, + panelState, + eventBus, + } = props; + const [showLabels, setShowLabels] = useState(store.getBool(SETTINGS_KEYS.showLabels, false)); + const [showTime, setShowTime] = useState(store.getBool(SETTINGS_KEYS.showTime, true)); + const [wrapLogMessage, setWrapLogMessage] = useState(store.getBool(SETTINGS_KEYS.wrapLogMessage, true)); + const [prettifyLogMessage, setPrettifyLogMessage] = useState( + store.getBool(SETTINGS_KEYS.prettifyLogMessage, false) + ); + const [dedupStrategy, setDedupStrategy] = useState(LogsDedupStrategy.none); + const [hiddenLogLevels, setHiddenLogLevels] = useState([]); + const [logsSortOrder, setLogsSortOrder] = useState( + store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending + ); + const [isFlipping, setIsFlipping] = useState(false); + const [displayedFields, setDisplayedFields] = useState([]); + const [forceEscape, setForceEscape] = useState(false); + const [contextOpen, setContextOpen] = useState(false); + const [contextRow, setContextRow] = useState(undefined); + const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState('Pin to content outline'); + const [visualisationType, setVisualisationType] = useState( + panelState?.logs?.visualisationType ?? getDefaultVisualisationType() + ); + const [scrollIntoView, setScrollIntoView] = useState<((element: HTMLElement) => void) | undefined>(undefined); + const logsContainerRef = useRef(undefined); + const dispatch = useDispatch(); + const previousLoading = usePrevious(loading); - state: State = { - showLabels: store.getBool(SETTINGS_KEYS.showLabels, false), - showTime: store.getBool(SETTINGS_KEYS.showTime, true), - wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true), - prettifyLogMessage: store.getBool(SETTINGS_KEYS.prettifyLogMessage, false), - dedupStrategy: LogsDedupStrategy.none, - hiddenLogLevels: [], - logsSortOrder: store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending, - isFlipping: false, - displayedFields: [], - forceEscape: false, - contextOpen: false, - contextRow: undefined, - tableFrame: undefined, - visualisationType: this.props.panelState?.logs?.visualisationType ?? getDefaultVisualisationType(), - logsContainer: undefined, - pinLineButtonTooltipTitle: 'Pin to content outline', - }; + const logsVolumeEventBus = eventBus.newScopedBus('logsvolume', { onlyLocal: false }); + const { register, unregister, outlineItems, updateItem, unregisterAllChildren } = useContentOutlineContext() ?? {}; + const flipOrderTimer = useRef(undefined); + const cancelFlippingTimer = useRef(undefined); + const toggleLegendRef = useRef<(name: string, mode: SeriesVisibilityChangeMode) => void>(() => {}); + const topLogsRef = useRef(null); - constructor(props: Props) { - super(props); - this.logsVolumeEventBus = props.eventBus.newScopedBus('logsvolume', { onlyLocal: false }); - } + const tableHeight = getLogsTableHeight(); + const styles = getStyles(theme, wrapLogMessage, tableHeight); + const hasData = logRows && logRows.length > 0; + const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; - componentDidMount(): void { - this.registerLogLevelsWithContentOutline(); - } - - componentWillUnmount() { - if (this.flipOrderTimer) { - window.clearTimeout(this.flipOrderTimer); - } - - if (this.cancelFlippingTimer) { - window.clearTimeout(this.cancelFlippingTimer); - } - - // If we're unmounting logs (e.g. switching to another datasource), we need to remove the table specific panel state, otherwise it will persist in the explore url - if ( - this.props?.panelState?.logs?.columns || - this.props?.panelState?.logs?.refId || - this.props?.panelState?.logs?.labelFieldName - ) { - dispatch( - changePanelState(this.props.exploreId, 'logs', { - ...this.props.panelState?.logs, - columns: undefined, - visualisationType: this.state.visualisationType, - labelFieldName: undefined, - refId: undefined, - }) - ); - } - } - - registerLogLevelsWithContentOutline = () => { + const registerLogLevelsWithContentOutline = useCallback(() => { const levelsArr = Object.keys(LogLevelColor); - const logVolumeDataFrames = new Set(this.props.logsVolumeData?.data); + const logVolumeDataFrames = new Set(logsVolumeData?.data); // TODO remove this once filtering multiple log volumes is supported - const numberOfLogVolumes = this.getNumberOfLogVolumes(); + const logVolData = logsVolumeData?.data.filter( + (frame: DataFrame) => frame.meta?.dataTopic !== DataTopic.Annotations + ); + const grouped = groupBy(logVolData, 'meta.custom.datasourceName'); + const numberOfLogVolumes = Object.keys(grouped).length; // clean up all current log levels - const logsParent = this.context?.outlineItems.find((item) => item.panelId === 'Logs' && item.level === 'root'); - if (logsParent) { - this.context?.unregisterAllChildren(logsParent.id, 'filter'); + if (unregisterAllChildren) { + unregisterAllChildren((items) => { + const logsParent = items?.find((item) => item.panelId === 'Logs' && item.level === 'root'); + return logsParent?.id; + }, 'filter'); } // check if we have dataFrames that return the same level @@ -255,279 +247,322 @@ class UnthemedLogs extends PureComponent { const logLevels = new Set(sortedLLArray); - if (logLevels.size > 1 && this.props.logsVolumeEnabled && numberOfLogVolumes === 1) { + if (logLevels.size > 1 && logsVolumeEnabled && numberOfLogVolumes === 1) { logLevels.forEach((level) => { - const allLevelsSelected = this.state.hiddenLogLevels.length === 0; - const currentLevelSelected = !this.state.hiddenLogLevels.find((hiddenLevel) => hiddenLevel === level.levelStr); - this.context?.register({ - title: level.levelStr, - icon: 'gf-logs', - panelId: 'Logs', - level: 'child', - type: 'filter', - highlight: currentLevelSelected && !allLevelsSelected, - onClick: (e: React.MouseEvent) => { - this.toggleLegendRef.current?.(level.levelStr, mapMouseEventToMode(e)); - }, - ref: null, - color: LogLevelColor[level.logLevel], - }); + const allLevelsSelected = hiddenLogLevels.length === 0; + const currentLevelSelected = !hiddenLogLevels.find((hiddenLevel) => hiddenLevel === level.levelStr); + if (register) { + register({ + title: level.levelStr, + icon: 'gf-logs', + panelId: 'Logs', + level: 'child', + type: 'filter', + highlight: currentLevelSelected && !allLevelsSelected, + onClick: (e: React.MouseEvent) => { + toggleLegendRef.current?.(level.levelStr, mapMouseEventToMode(e)); + }, + ref: null, + color: LogLevelColor[level.logLevel], + }); + } }); } - }; + }, [logsVolumeData?.data, unregisterAllChildren, logsVolumeEnabled, hiddenLogLevels, register, toggleLegendRef]); - updatePanelState = (logsPanelState: Partial) => { - const state: ExploreItemState | undefined = getState().explore.panes[this.props.exploreId]; - if (state?.panelsState) { - dispatch( - changePanelState(this.props.exploreId, 'logs', { - ...state.panelsState.logs, - columns: logsPanelState.columns ?? this.props.panelState?.logs?.columns, - visualisationType: logsPanelState.visualisationType ?? this.state.visualisationType, - labelFieldName: logsPanelState.labelFieldName, - refId: logsPanelState.refId ?? this.props.panelState?.logs?.refId, - }) - ); - } - }; - - getNumberOfLogVolumes() { - const data = this.props.logsVolumeData?.data.filter( - (frame: DataFrame) => frame.meta?.dataTopic !== DataTopic.Annotations - ); - const grouped = groupBy(data, 'meta.custom.datasourceName'); - const numberOfLogVolumes = Object.keys(grouped).length; - return numberOfLogVolumes; - } - - componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { - if (this.props.loading && !prevProps.loading && this.props.panelState?.logs?.id) { + useEffect(() => { + if (loading && !previousLoading && panelState?.logs?.id) { // loading stopped, so we need to remove any permalinked log lines - delete this.props.panelState.logs.id; + delete panelState.logs.id; dispatch( - changePanelState(this.props.exploreId, 'logs', { - ...this.props.panelState, + changePanelState(exploreId, 'logs', { + ...panelState, }) ); } - if (this.props.panelState?.logs?.visualisationType !== prevProps.panelState?.logs?.visualisationType) { - const visualisationType = this.props.panelState?.logs?.visualisationType ?? getDefaultVisualisationType(); + }, [dispatch, exploreId, loading, panelState, previousLoading]); - this.setState({ - visualisationType: visualisationType, + useEffect(() => { + const visualisationType = panelState?.logs?.visualisationType ?? getDefaultVisualisationType(); + setVisualisationType(visualisationType); + + store.set(visualisationTypeKey, visualisationType); + }, [panelState?.logs?.visualisationType]); + + useEffect(() => { + registerLogLevelsWithContentOutline(); + }, [logsVolumeData?.data, hiddenLogLevels, registerLogLevelsWithContentOutline]); + + useUnmount(() => { + if (flipOrderTimer) { + window.clearTimeout(flipOrderTimer.current); + } + if (cancelFlippingTimer) { + window.clearTimeout(cancelFlippingTimer.current); + } + }); + + useUnmount(() => { + // If we're unmounting logs (e.g. switching to another datasource), we need to remove the table specific panel state, otherwise it will persist in the explore url + if (panelState?.logs?.columns || panelState?.logs?.refId || panelState?.logs?.labelFieldName) { + dispatch( + changePanelState(exploreId, 'logs', { + ...panelState?.logs, + columns: undefined, + visualisationType: visualisationType, + labelFieldName: undefined, + refId: undefined, + }) + ); + } + }); + + const updatePanelState = useCallback( + (logsPanelState: Partial) => { + const state: ExploreItemState | undefined = getState().explore.panes[exploreId]; + if (state?.panelsState) { + dispatch( + changePanelState(exploreId, 'logs', { + ...state.panelsState.logs, + columns: logsPanelState.columns ?? panelState?.logs?.columns, + visualisationType: logsPanelState.visualisationType ?? visualisationType, + labelFieldName: logsPanelState.labelFieldName, + refId: logsPanelState.refId ?? panelState?.logs?.refId, + }) + ); + } + }, + [dispatch, exploreId, panelState?.logs?.columns, panelState?.logs?.refId, visualisationType] + ); + + // actions + const onLogRowHover = useCallback( + (row?: LogRowModel) => { + if (!row) { + props.eventBus.publish(new DataHoverClearEvent()); + } else { + props.eventBus.publish( + new DataHoverEvent({ + point: { + time: row.timeEpochMs, + }, + }) + ); + } + }, + [props.eventBus] + ); + + const onLogsContainerRef = useCallback( + (node: HTMLDivElement) => { + logsContainerRef.current = node; + + // In theory this should be just a function passed down to LogRows but: + // - LogRow.componentDidMount which calls scrollIntoView is called BEFORE the logsContainerRef is set + // - the if check below if (logsContainerRef.current) was falsy and scrolling doesn't happen + // - and LogRow.scrollToLogRow marks the line as scrolled anyway (and won't perform scrolling when the ref is set) + // - see more details in https://github.com/facebook/react/issues/29897 + // We can change it once LogRow is converted into a functional component + setScrollIntoView(() => (element: HTMLElement) => { + if (config.featureToggles.logsInfiniteScrolling) { + if (logsContainerRef.current) { + topLogsRef.current?.scrollIntoView(); + logsContainerRef.current.scroll({ + behavior: 'smooth', + top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, + }); + } + + return; + } + const scrollElement = props.scrollElement; + + if (scrollElement) { + scrollElement.scroll({ + behavior: 'smooth', + top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, + }); + } }); - store.set(visualisationTypeKey, visualisationType); - } + }, + [props.scrollElement] + ); - if ( - prevProps.logsVolumeData?.data !== this.props.logsVolumeData?.data || - prevState.hiddenLogLevels !== this.state.hiddenLogLevels - ) { - this.registerLogLevelsWithContentOutline(); - } - } - - onLogRowHover = (row?: LogRowModel) => { - if (!row) { - this.props.eventBus.publish(new DataHoverClearEvent()); - } else { - this.props.eventBus.publish( - new DataHoverEvent({ - point: { - time: row.timeEpochMs, - }, - }) - ); - } - }; - - onLogsContainerRef = (node: HTMLDivElement) => { - this.setState({ logsContainer: node }); - }; - - onChangeLogsSortOrder = () => { - this.setState({ isFlipping: true }); + const onChangeLogsSortOrder = () => { + setIsFlipping(true); // we are using setTimeout here to make sure that disabled button is rendered before the rendering of reordered logs - this.flipOrderTimer = window.setTimeout(() => { - this.setState((prevState) => { - const newSortOrder = - prevState.logsSortOrder === LogsSortOrder.Descending ? LogsSortOrder.Ascending : LogsSortOrder.Descending; - store.set(SETTINGS_KEYS.logsSortOrder, newSortOrder); - return { logsSortOrder: newSortOrder }; - }); + flipOrderTimer.current = window.setTimeout(() => { + const newSortOrder = + logsSortOrder === LogsSortOrder.Descending ? LogsSortOrder.Ascending : LogsSortOrder.Descending; + store.set(SETTINGS_KEYS.logsSortOrder, newSortOrder); + setLogsSortOrder(newSortOrder); }, 0); - this.cancelFlippingTimer = window.setTimeout(() => this.setState({ isFlipping: false }), 1000); + cancelFlippingTimer.current = window.setTimeout(() => setIsFlipping(false), 1000); }; - onEscapeNewlines = () => { - this.setState((prevState) => ({ - forceEscape: !prevState.forceEscape, - })); - }; + const onEscapeNewlines = useCallback(() => { + setForceEscape(!forceEscape); + }, [forceEscape]); - onChangeVisualisation = (visualisation: LogsVisualisationType) => { - this.setState(() => ({ - visualisationType: visualisation, - })); - const payload = { - ...this.props.panelState?.logs, - visualisationType: visualisation, - }; - this.updatePanelState(payload); + const onChangeVisualisation = useCallback( + (visualisation: LogsVisualisationType) => { + setVisualisationType(visualisation); + const payload = { + ...panelState?.logs, + visualisationType: visualisation, + }; + updatePanelState(payload); - reportInteraction('grafana_explore_logs_visualisation_changed', { - newVisualizationType: visualisation, - datasourceType: this.props.datasourceType ?? 'unknown', - defaultVisualisationType: config.featureToggles.logsExploreTableDefaultVisualization ? 'table' : 'logs', - }); - }; + reportInteraction('grafana_explore_logs_visualisation_changed', { + newVisualizationType: visualisation, + datasourceType: props.datasourceType ?? 'unknown', + defaultVisualisationType: config.featureToggles.logsExploreTableDefaultVisualization ? 'table' : 'logs', + }); + }, + [panelState?.logs, props.datasourceType, updatePanelState] + ); - onChangeDedup = (dedupStrategy: LogsDedupStrategy) => { - reportInteraction('grafana_explore_logs_deduplication_clicked', { - deduplicationType: dedupStrategy, - datasourceType: this.props.datasourceType, - }); - this.setState({ dedupStrategy }); - }; + const onChangeDedup = useCallback( + (dedupStrategy: LogsDedupStrategy) => { + reportInteraction('grafana_explore_logs_deduplication_clicked', { + deduplicationType: dedupStrategy, + datasourceType: props.datasourceType, + }); + setDedupStrategy(dedupStrategy); + }, + [props.datasourceType] + ); - onChangeLabels = (event: React.ChangeEvent) => { + const onChangeLabels = useCallback((event: React.ChangeEvent) => { const { target } = event; if (target) { const showLabels = target.checked; - this.setState({ - showLabels, - }); + + setShowLabels(showLabels); store.set(SETTINGS_KEYS.showLabels, showLabels); } - }; + }, []); - onChangeTime = (event: React.ChangeEvent) => { + const onChangeShowTime = useCallback((event: React.ChangeEvent) => { const { target } = event; if (target) { const showTime = target.checked; - this.setState({ - showTime, - }); + + setShowTime(showTime); store.set(SETTINGS_KEYS.showTime, showTime); } - }; + }, []); - onChangeWrapLogMessage = (event: React.ChangeEvent) => { + const onChangeWrapLogMessage = useCallback((event: React.ChangeEvent) => { const { target } = event; if (target) { const wrapLogMessage = target.checked; - this.setState({ - wrapLogMessage, - }); + + setWrapLogMessage(wrapLogMessage); store.set(SETTINGS_KEYS.wrapLogMessage, wrapLogMessage); } - }; + }, []); - onChangePrettifyLogMessage = (event: React.ChangeEvent) => { + const onChangePrettifyLogMessage = useCallback((event: React.ChangeEvent) => { const { target } = event; if (target) { const prettifyLogMessage = target.checked; - this.setState({ - prettifyLogMessage, - }); + + setPrettifyLogMessage(prettifyLogMessage); store.set(SETTINGS_KEYS.prettifyLogMessage, prettifyLogMessage); } - }; + }, []); - onToggleLogLevel = (hiddenRawLevels: string[]) => { + const onToggleLogLevel = useCallback((hiddenRawLevels: string[]) => { const hiddenLogLevels = hiddenRawLevels.map((level) => getLogLevelFromKey(level)); - this.setState({ hiddenLogLevels }); - }; + setHiddenLogLevels(hiddenLogLevels); + }, []); - onToggleLogsVolumeCollapse = (collapsed: boolean) => { - this.props.onSetLogsVolumeEnabled(!collapsed); - reportInteraction('grafana_explore_logs_histogram_toggle_clicked', { - datasourceType: this.props.datasourceType, - type: !collapsed ? 'open' : 'close', - }); - }; - - onClickScan = (event: React.SyntheticEvent) => { - event.preventDefault(); - if (this.props.onStartScanning) { - this.props.onStartScanning(); - reportInteraction('grafana_explore_logs_scanning_button_clicked', { - type: 'start', - datasourceType: this.props.datasourceType, + const onToggleLogsVolumeCollapse = useCallback( + (collapsed: boolean) => { + props.onSetLogsVolumeEnabled(!collapsed); + reportInteraction('grafana_explore_logs_histogram_toggle_clicked', { + datasourceType: props.datasourceType, + type: !collapsed ? 'open' : 'close', }); - } - }; + }, + [props] + ); - onClickStopScan = (event: React.SyntheticEvent) => { - event.preventDefault(); - if (this.props.onStopScanning) { - this.props.onStopScanning(); - } - }; + const onClickScan = useCallback( + (event: React.SyntheticEvent) => { + event.preventDefault(); + if (props.onStartScanning) { + props.onStartScanning(); + reportInteraction('grafana_explore_logs_scanning_button_clicked', { + type: 'start', + datasourceType: props.datasourceType, + }); + } + }, + [props] + ); - showField = (key: string) => { - const index = this.state.displayedFields.indexOf(key); + const onClickStopScan = useCallback( + (event: React.SyntheticEvent) => { + event.preventDefault(); + if (props.onStopScanning) { + props.onStopScanning(); + } + }, + [props] + ); - if (index === -1) { - this.setState((state) => { - return { - displayedFields: state.displayedFields.concat(key), - }; - }); - } - }; + const showField = useCallback( + (key: string) => { + const index = displayedFields.indexOf(key); - hideField = (key: string) => { - const index = this.state.displayedFields.indexOf(key); - if (index > -1) { - this.setState((state) => { - return { - displayedFields: state.displayedFields.filter((k) => key !== k), - }; - }); - } - }; + if (index === -1) { + setDisplayedFields(displayedFields.concat(key)); + } + }, + [displayedFields] + ); - clearDetectedFields = () => { - this.setState((state) => { - return { - displayedFields: [], - }; + const hideField = useCallback( + (key: string) => { + const index = displayedFields.indexOf(key); + if (index > -1) { + setDisplayedFields(displayedFields.filter((k) => key !== k)); + } + }, + [displayedFields] + ); + + const clearDetectedFields = useCallback(() => { + setDisplayedFields([]); + }, []); + + const onCloseCallbackRef = useRef<() => void>(() => {}); + + let onCloseContext = useCallback(() => { + setContextOpen(false); + setContextRow(undefined); + reportInteraction('grafana_explore_logs_log_context_closed', { + datasourceType: contextRow?.datasourceType, + logRowUid: contextRow?.uid, }); - }; + onCloseCallbackRef?.current(); + }, [contextRow?.datasourceType, contextRow?.uid, onCloseCallbackRef]); - onCloseContext = () => { - this.setState({ - contextOpen: false, - contextRow: undefined, - }); - }; - - onOpenContext = (row: LogRowModel, onClose: () => void) => { + const onOpenContext = (row: LogRowModel, onClose: () => void) => { // we are setting the `contextOpen` open state and passing it down to the `LogRow` in order to highlight the row when a LogContext is open - this.setState({ - contextOpen: true, - contextRow: row, - }); + setContextOpen(true); + setContextRow(row); reportInteraction('grafana_explore_logs_log_context_opened', { datasourceType: row.datasourceType, logRowUid: row.uid, }); - this.onCloseContext = () => { - this.setState({ - contextOpen: false, - contextRow: undefined, - }); - reportInteraction('grafana_explore_logs_log_context_closed', { - datasourceType: row.datasourceType, - logRowUid: row.uid, - }); - onClose(); - }; + onCloseCallbackRef.current = onClose; }; - getPreviousLog(row: LogRowModel, allLogs: LogRowModel[]): LogRowModel | null { + const getPreviousLog = useCallback((row: LogRowModel, allLogs: LogRowModel[]) => { for (let i = allLogs.indexOf(row) - 1; i >= 0; i--) { if (allLogs[i].timeEpochMs > row.timeEpochMs) { return allLogs[i]; @@ -535,39 +570,42 @@ class UnthemedLogs extends PureComponent { } return null; - } + }, []); - getPermalinkRange(row: LogRowModel) { - const range = { - from: new Date(this.props.absoluteRange.from).toISOString(), - to: new Date(this.props.absoluteRange.to).toISOString(), - }; - if (!config.featureToggles.logsInfiniteScrolling) { - return range; - } - - // With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust - // Look for the previous sibling log, and use its timestamp - const allLogs = this.props.logRows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId); - const prevLog = this.getPreviousLog(row, allLogs); - - if (row.timeEpochMs > this.props.absoluteRange.to && !prevLog) { - // Because there's no sibling and the current `to` is oldest than the log, we have no reference we can use for the interval - // This only happens when you scroll into the future and you want to share the first log of the list - return { - from: new Date(this.props.absoluteRange.from).toISOString(), - // Slide 1ms otherwise it's very likely to be omitted in the results - to: new Date(row.timeEpochMs + 1).toISOString(), + const getPermalinkRange = useCallback( + (row: LogRowModel) => { + const range = { + from: new Date(absoluteRange.from).toISOString(), + to: new Date(absoluteRange.to).toISOString(), }; - } + if (!config.featureToggles.logsInfiniteScrolling) { + return range; + } - return { - from: new Date(this.props.absoluteRange.from).toISOString(), - to: new Date(prevLog ? prevLog.timeEpochMs : this.props.absoluteRange.to).toISOString(), - }; - } + // With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust + // Look for the previous sibling log, and use its timestamp + const allLogs = logRows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId); + const prevLog = getPreviousLog(row, allLogs); - onPermalinkClick = async (row: LogRowModel) => { + if (row.timeEpochMs > absoluteRange.to && !prevLog) { + // Because there's no sibling and the current `to` is oldest than the log, we have no reference we can use for the interval + // This only happens when you scroll into the future and you want to share the first log of the list + return { + from: new Date(absoluteRange.from).toISOString(), + // Slide 1ms otherwise it's very likely to be omitted in the results + to: new Date(row.timeEpochMs + 1).toISOString(), + }; + } + + return { + from: new Date(absoluteRange.from).toISOString(), + to: new Date(prevLog ? prevLog.timeEpochMs : absoluteRange.to).toISOString(), + }; + }, + [absoluteRange.from, absoluteRange.to, getPreviousLog, logRows] + ); + + const onPermalinkClick = async (row: LogRowModel) => { // this is an extra check, to be sure that we are not // creating permalinks for logs without an id-field. // normally it should never happen, because we do not @@ -577,12 +615,12 @@ class UnthemedLogs extends PureComponent { } // get explore state, add log-row-id and make timerange absolute - const urlState = getUrlStateFromPaneState(getState().explore.panes[this.props.exploreId]!); + const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!); urlState.panelsState = { - ...this.props.panelState, - logs: { id: row.uid, visualisationType: this.state.visualisationType ?? getDefaultVisualisationType() }, + ...panelState, + logs: { id: row.uid, visualisationType: visualisationType ?? getDefaultVisualisationType() }, }; - urlState.range = this.getPermalinkRange(row); + urlState.range = getPermalinkRange(row); // append changed urlState to baseUrl const serializedState = serializeStateToUrlParam(urlState); @@ -597,92 +635,40 @@ class UnthemedLogs extends PureComponent { }); }; - scrollIntoView = (element: HTMLElement) => { + const scrollToTopLogs = useCallback(() => { if (config.featureToggles.logsInfiniteScrolling) { - if (this.state.logsContainer) { - this.topLogsRef.current?.scrollIntoView(); - this.state.logsContainer.scroll({ - behavior: 'smooth', - top: this.state.logsContainer.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, - }); - } - - return; - } - const { scrollElement } = this.props; - - if (scrollElement) { - scrollElement.scroll({ - behavior: 'smooth', - top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2, - }); - } - }; - - checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => { - return !!logRows.some((r) => r.hasUnescapedContent); - }); - - dedupRows = memoizeOne((logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => { - const dedupedRows = dedupLogRows(logRows, dedupStrategy); - const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0); - return { dedupedRows, dedupCount }; - }); - - filterRows = memoizeOne((logRows: LogRowModel[], hiddenLogLevels: LogLevel[]) => { - return filterLogLevels(logRows, new Set(hiddenLogLevels)); - }); - - createNavigationRange = memoizeOne((logRows: LogRowModel[]): { from: number; to: number } | undefined => { - if (!logRows || logRows.length === 0) { - return undefined; - } - const firstTimeStamp = logRows[0].timeEpochMs; - const lastTimeStamp = logRows[logRows.length - 1].timeEpochMs; - - if (lastTimeStamp < firstTimeStamp) { - return { from: lastTimeStamp, to: firstTimeStamp }; - } - - return { from: firstTimeStamp, to: lastTimeStamp }; - }); - - scrollToTopLogs = () => { - if (config.featureToggles.logsInfiniteScrolling) { - if (this.state.logsContainer) { - this.state.logsContainer.scroll({ + if (logsContainerRef.current) { + logsContainerRef.current.scroll({ behavior: 'auto', top: 0, }); } } - this.topLogsRef.current?.scrollIntoView(); - }; + topLogsRef.current?.scrollIntoView(); + }, [logsContainerRef, topLogsRef]); - onPinToContentOutlineClick = (row: LogRowModel) => { - if (this.getPinnedLogsCount() === PINNED_LOGS_LIMIT) { - this.setState({ - pinLineButtonTooltipTitle: ( - - ❗️ - - Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another. - - - ), - }); + const onPinToContentOutlineClick = (row: LogRowModel) => { + if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) { + setPinLineButtonTooltipTitle( + + ❗️ + + Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another. + + + ); return; } // find the Logs parent item - const logsParent = this.context?.outlineItems.find((item) => item.panelId === 'Logs' && item.level === 'root'); + const logsParent = outlineItems?.find((item) => item.panelId === 'Logs' && item.level === 'root'); //update the parent's expanded state - if (logsParent) { - this.context?.updateItem(logsParent.id, { expanded: true }); + if (logsParent && updateItem) { + updateItem(logsParent.id, { expanded: true }); } - this.context?.register({ + register?.({ icon: 'gf-logs', title: 'Pinned log', panelId: 'Logs', @@ -690,362 +676,306 @@ class UnthemedLogs extends PureComponent { ref: null, color: LogLevelColor[row.logLevel], childOnTop: true, - onClick: () => this.onOpenContext(row, () => {}), + onClick: () => onOpenContext(row, () => {}), onRemove: (id: string) => { - this.context?.unregister(id); - if (this.getPinnedLogsCount() < PINNED_LOGS_LIMIT) { - this.setState({ - pinLineButtonTooltipTitle: 'Pin to content outline', - }); + unregister?.(id); + if (getPinnedLogsCount() < PINNED_LOGS_LIMIT) { + setPinLineButtonTooltipTitle('Pin to content outline'); } }, }); - this.props.onPinLineCallback?.(); + props.onPinLineCallback?.(); }; - getPinnedLogsCount = () => { - const logsParent = this.context?.outlineItems.find((item) => item.panelId === 'Logs' && item.level === 'root'); + const getPinnedLogsCount = () => { + const logsParent = outlineItems?.find((item) => item.panelId === 'Logs' && item.level === 'root'); return logsParent?.children?.filter((child) => child.title === 'Pinned log').length ?? 0; }; - render() { - const { - width, - splitOpen, - logRows, - logsMeta, - logsVolumeEnabled, - logsVolumeData, - loadLogsVolumeData, - loading = false, - onClickFilterLabel, - onClickFilterOutLabel, - timeZone, - scanning, - scanRange, - showContextToggle, - absoluteRange, - onChangeTime, - getFieldLinks, - theme, - logsQueries, - clearCache, - addResultsToCache, - exploreId, - getRowContext, - getLogRowContextUi, - getRowContextQuery, - loadMoreLogs, - } = this.props; + const hasUnescapedContent = checkUnescapedContent(logRows); + const filteredLogs = filterRows(logRows, hiddenLogLevels); + const { dedupedRows, dedupCount } = dedupRows(filteredLogs, dedupStrategy); + const navigationRange = createNavigationRange(logRows); - const { - showLabels, - showTime, - wrapLogMessage, - prettifyLogMessage, - dedupStrategy, - hiddenLogLevels, - logsSortOrder, - isFlipping, - displayedFields, - forceEscape, - contextOpen, - contextRow, - } = this.state; - - const tableHeight = getLogsTableHeight(); - const styles = getStyles(theme, wrapLogMessage, tableHeight); - const hasData = logRows && logRows.length > 0; - const hasUnescapedContent = this.checkUnescapedContent(logRows); - - const filteredLogs = this.filterRows(logRows, hiddenLogLevels); - const { dedupedRows, dedupCount } = this.dedupRows(filteredLogs, dedupStrategy); - const navigationRange = this.createNavigationRange(logRows); - - const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; - - return ( - <> - {getRowContext && contextRow && ( - getRowContext(row, contextRow, options)} - getRowContextQuery={getRowContextQuery} - getLogRowContextUi={getLogRowContextUi} - logsSortOrder={logsSortOrder} + return ( + <> + {getRowContext && contextRow && ( + getRowContext(row, contextRow, options)} + getRowContextQuery={getRowContextQuery} + getLogRowContextUi={getLogRowContextUi} + logsSortOrder={logsSortOrder} + timeZone={timeZone} + /> + )} + + {logsVolumeEnabled && ( + onToggleLogsVolumeCollapse(true)} /> )} - - {logsVolumeEnabled && ( - this.onToggleLogsVolumeCollapse(true)} - /> - )} - - - - - ) - ) : null, - ]} - title={'Logs'} - actions={ - <> - {config.featureToggles.logsExploreTableVisualisation && ( -
- -
- )} - - } - loadingState={loading ? LoadingState.Loading : LoadingState.Done} - > -
- {this.state.visualisationType !== 'table' && ( -
- - - - - - - - - - - - - - - ({ - label: capitalize(dedupType), - value: dedupType, - description: LogsDedupDescription[dedupType], - }))} - value={dedupStrategy} - onChange={this.onChangeDedup} - className={styles.radioButtons} - /> - - - -
- - - -
-
- )} -
- -
-
- {this.state.visualisationType === 'table' && hasData && ( -
- {/* Width should be full width minus logs navigation and padding */} - + + + + ) + ) : null, + ]} + title={'Logs'} + actions={ + <> + {config.featureToggles.logsExploreTableVisualisation && ( +
+
)} - {this.state.visualisationType === 'logs' && hasData && ( -
- - + } + loadingState={loading ? LoadingState.Loading : LoadingState.Done} + > +
+ {visualisationType !== 'table' && ( +
+ + + - + + + + + + + + + + + + ({ + label: capitalize(dedupType), + value: dedupType, + description: LogsDedupDescription[dedupType], + }))} + value={dedupStrategy} + onChange={onChangeDedup} + className={styles.radioButtons} + /> + + + +
+ + +
- )} - {!loading && !hasData && !scanning && ( -
-
- No logs found. - -
+
+ )} +
+ +
+
+ {visualisationType === 'table' && hasData && ( +
+ {/* Width should be full width minus logs navigation and padding */} + +
+ )} + {visualisationType === 'logs' && hasData && ( +
+ + + +
+ )} + {!loading && !hasData && !scanning && ( +
+
+ No logs found. +
- )} - {scanning && ( -
-
- {scanText} - -
+
+ )} + {scanning && ( +
+
+ {scanText} +
- )} - -
- - - ); - } -} +
+ )} + +
+ + + ); +}; export const Logs = withTheme2(UnthemedLogs); @@ -1113,3 +1043,31 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n }), }; }; + +const checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => { + return logRows.some((r) => r.hasUnescapedContent); +}); + +const dedupRows = memoizeOne((logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => { + const dedupedRows = dedupLogRows(logRows, dedupStrategy); + const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0); + return { dedupedRows, dedupCount }; +}); + +const filterRows = memoizeOne((logRows: LogRowModel[], hiddenLogLevels: LogLevel[]) => { + return filterLogLevels(logRows, new Set(hiddenLogLevels)); +}); + +const createNavigationRange = memoizeOne((logRows: LogRowModel[]): { from: number; to: number } | undefined => { + if (!logRows || logRows.length === 0) { + return undefined; + } + const firstTimeStamp = logRows[0].timeEpochMs; + const lastTimeStamp = logRows[logRows.length - 1].timeEpochMs; + + if (lastTimeStamp < firstTimeStamp) { + return { from: lastTimeStamp, to: firstTimeStamp }; + } + + return { from: firstTimeStamp, to: lastTimeStamp }; +}); diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 2f453f17c25..306fc6ed3f1 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -270,7 +270,7 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction): }; } - if (initializeExplore.pending.match(action)) { + if (initializeExplore?.pending.match(action)) { const initialPanes = Object.entries(state.panes); const before = initialPanes.slice(0, action.meta.arg.position); const after = initialPanes.slice(before.length); From 1cc58d19f56ef25952b48222237563301c013970 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Thu, 20 Jun 2024 09:07:15 -0400 Subject: [PATCH 048/122] ci: Only run vuln scanner when Go deps are updated (#89433) Signed-off-by: Dave Henderson --- .github/workflows/trivy-scan.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index b68da3bc327..06d56a1ef7c 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -1,9 +1,14 @@ name: Trivy Scan on: pull_request: + # only run on PRs where go.mod/go.sum/etc have been updated + paths: + - go.* push: branches: - main + paths: + - go.* jobs: trivy-scan: @@ -25,6 +30,8 @@ jobs: vuln-type: 'os,library' severity: 'CRITICAL,HIGH' trivyignores: .trivyignore + # for the PR check, ignore JS-related issues + skip-files: 'yarn.lock,package.json' - name: Run Trivy vulnerability scanner (SARIF) uses: aquasecurity/trivy-action@0.22.0 with: From 6834038e9164072229fb35bdd04d3f39a08686e9 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 20 Jun 2024 16:11:21 +0300 Subject: [PATCH 049/122] Storage: Add new resource module and move some utilities (#89443) --- .github/CODEOWNERS | 1 + go.work | 1 + go.work.sum | 1 + .../store/entity/sqlstash/sql_storage_server.go | 5 +++-- .../unified/resource}/broadcaster.go | 14 +++++++------- .../unified/resource}/broadcaster_test.go | 4 ++-- pkg/storage/unified/resource/go.mod | 14 ++++++++++++++ pkg/storage/unified/resource/go.sum | 8 ++++++++ 8 files changed, 37 insertions(+), 11 deletions(-) rename pkg/{services/store/entity/sqlstash => storage/unified/resource}/broadcaster.go (97%) rename pkg/{services/store/entity/sqlstash => storage/unified/resource}/broadcaster_test.go (96%) create mode 100644 pkg/storage/unified/resource/go.mod create mode 100644 pkg/storage/unified/resource/go.sum diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 61359aed23a..a2fc88371ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -107,6 +107,7 @@ /pkg/apimachinery/identity/ @grafana/identity-squad /pkg/apimachinery/errutil/ @grafana/grafana-backend-group /pkg/promlib @grafana/observability-metrics +/pkg/storage/ @grafana/grafana-search-and-storage /pkg/services/annotations/ @grafana/grafana-search-and-storage /pkg/services/apikey/ @grafana/identity-squad /pkg/services/cleanup/ @grafana/grafana-backend-group diff --git a/go.work b/go.work index 2f858b9675e..67f2d6728c2 100644 --- a/go.work +++ b/go.work @@ -7,6 +7,7 @@ use ( ./pkg/build ./pkg/build/wire ./pkg/promlib + ./pkg/storage/unified/resource ./pkg/util/xorm ) diff --git a/go.work.sum b/go.work.sum index d8edfcf36bd..d971095824b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -942,6 +942,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.228.0/go.mod h1:u4K9vVN6eU86loO68977 github.com/grafana/grafana-plugin-sdk-go v0.229.0/go.mod h1:6V6ikT4ryva8MrAp7Bdz5fTJx3/ztzKvpMJFfpzr4CI= github.com/grafana/grafana-plugin-sdk-go v0.231.1-0.20240523124942-62dae9836284/go.mod h1:bNgmNmub1I7Mc8dzIncgNqHC5jTgSZPPHlZ3aG8HKJQ= github.com/grafana/grafana-plugin-sdk-go v0.234.0/go.mod h1:FlXjmBESxaD6Hoi8ojWLkH007nyjtJM3XC8SpwzF/YE= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240613114114-5e2f08de316d/go.mod h1:adT8O7k6ZSzUKjAC4WS6VfWlCE4G1VavPwSXVhvScCs= github.com/grafana/grafana/pkg/promlib v0.0.3/go.mod h1:3El4NlsfALz8QQCbEGHGFvJUG+538QLMuALRhZ3pcoo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 0a7d7cce617..046ed69e4c7 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -25,6 +25,7 @@ import ( "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/entity/db" "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate" + "github.com/grafana/grafana/pkg/storage/unified/resource" ) const entityTable = "entity" @@ -75,7 +76,7 @@ type sqlEntityServer struct { db db.EntityDBInterface // needed to keep xorm engine in scope sess *session.SessionDB dialect migrator.Dialect - broadcaster Broadcaster[*entity.EntityWatchResponse] + broadcaster resource.Broadcaster[*entity.EntityWatchResponse] ctx context.Context // TODO: remove cancel context.CancelFunc tracer trace.Tracer @@ -141,7 +142,7 @@ func (s *sqlEntityServer) init() error { s.dialect = migrator.NewDialect(engine.DriverName()) // set up the broadcaster - s.broadcaster, err = NewBroadcaster(s.ctx, func(stream chan<- *entity.EntityWatchResponse) error { + s.broadcaster, err = resource.NewBroadcaster(s.ctx, func(stream chan<- *entity.EntityWatchResponse) error { // start the poller go s.poller(stream) diff --git a/pkg/services/store/entity/sqlstash/broadcaster.go b/pkg/storage/unified/resource/broadcaster.go similarity index 97% rename from pkg/services/store/entity/sqlstash/broadcaster.go rename to pkg/storage/unified/resource/broadcaster.go index 56a010bf2a0..547bfe576e8 100644 --- a/pkg/services/store/entity/sqlstash/broadcaster.go +++ b/pkg/storage/unified/resource/broadcaster.go @@ -1,4 +1,4 @@ -package sqlstash +package resource import ( "context" @@ -109,7 +109,7 @@ type broadcaster[T any] struct { // subscription management - cache Cache[T] + cache channelCache[T] subscribe chan chan T unsubscribe chan (<-chan T) subs map[<-chan T]chan T @@ -166,7 +166,7 @@ func (b *broadcaster[T]) init(ctx context.Context, connect ConnectFunc[T]) error // initialize our internal state b.shouldTerminate = ctx.Done() - b.cache = NewCache[T](ctx, 100) + b.cache = newChannelCache[T](ctx, 100) b.subscribe = make(chan chan T, 100) b.unsubscribe = make(chan (<-chan T), 100) b.subs = make(map[<-chan T]chan T) @@ -239,9 +239,9 @@ func (b *broadcaster[T]) stream(input <-chan T) { } } -const DefaultCacheSize = 100 +const defaultCacheSize = 100 -type Cache[T any] interface { +type channelCache[T any] interface { Len() int Add(item T) Get(i int) T @@ -260,12 +260,12 @@ type cache[T any] struct { ctx context.Context } -func NewCache[T any](ctx context.Context, size int) Cache[T] { +func newChannelCache[T any](ctx context.Context, size int) channelCache[T] { c := &cache[T]{} c.ctx = ctx if size <= 0 { - size = DefaultCacheSize + size = defaultCacheSize } c.size = size c.cache = make([]T, c.size) diff --git a/pkg/services/store/entity/sqlstash/broadcaster_test.go b/pkg/storage/unified/resource/broadcaster_test.go similarity index 96% rename from pkg/services/store/entity/sqlstash/broadcaster_test.go rename to pkg/storage/unified/resource/broadcaster_test.go index fc6a6369a2d..8eedfa01ce5 100644 --- a/pkg/services/store/entity/sqlstash/broadcaster_test.go +++ b/pkg/storage/unified/resource/broadcaster_test.go @@ -1,4 +1,4 @@ -package sqlstash +package resource import ( "context" @@ -8,7 +8,7 @@ import ( ) func TestCache(t *testing.T) { - c := NewCache[int](context.Background(), 10) + c := newChannelCache[int](context.Background(), 10) e := []int{} err := c.Range(func(i int) error { diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod new file mode 100644 index 00000000000..2ebcdfc5fed --- /dev/null +++ b/pkg/storage/unified/resource/go.mod @@ -0,0 +1,14 @@ +module github.com/grafana/grafana/pkg/storage/unified/resource + +go 1.21.10 + +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum new file mode 100644 index 00000000000..4d56934746b --- /dev/null +++ b/pkg/storage/unified/resource/go.sum @@ -0,0 +1,8 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 924a94cf80ff13de1ac09c6d4d75f91173f37d94 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Thu, 20 Jun 2024 15:51:17 +0200 Subject: [PATCH 050/122] New Select: Use virtual list (#89290) * use react-virtual * Render story with 100k items * Dyanmic height and TanStack * Remove weird item * Add numberOfOptions to story * Update class name * Update class name --- packages/grafana-ui/package.json | 1 + .../Combobox/Combobox.internal.story.tsx | 56 +++++++++++-- .../src/components/Combobox/Combobox.tsx | 84 ++++++++++++++++--- yarn.lock | 20 +++++ 4 files changed, 143 insertions(+), 18 deletions(-) diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 8a05ff5148e..da1bf32b7c3 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -61,6 +61,7 @@ "@react-aria/focus": "3.17.1", "@react-aria/overlays": "3.22.1", "@react-aria/utils": "3.24.1", + "@tanstack/react-virtual": "^3.5.1", "ansicolor": "1.1.100", "calculate-size": "1.1.1", "classnames": "2.5.1", diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx index e56a989a526..cf373415761 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx @@ -1,10 +1,15 @@ import { action } from '@storybook/addon-actions'; -import { Meta, StoryFn } from '@storybook/react'; -import React, { useState } from 'react'; +import { Meta, StoryFn, StoryObj } from '@storybook/react'; +import { Chance } from 'chance'; +import React, { ComponentProps, useMemo, useState } from 'react'; -import { Combobox } from './Combobox'; +import { Combobox, Option, Value } from './Combobox'; -const meta: Meta = { +const chance = new Chance(); + +type PropsAndCustomArgs = ComponentProps & { numberOfOptions: number }; + +const meta: Meta = { title: 'Forms/Combobox', component: Combobox, args: { @@ -28,9 +33,11 @@ const meta: Meta = { ], value: 'banana', }, + + render: (args) => , }; -export const Basic: StoryFn = (args) => { +const BasicWithState: StoryFn = (args) => { const [value, setValue] = useState(args.value); return ( = (args) => { ); }; +type Story = StoryObj; + +export const Basic: Story = {}; + +function generateOptions(amount: number): Option[] { + return Array.from({ length: amount }, () => ({ + label: chance.name(), + value: chance.guid(), + description: chance.sentence(), + })); +} + +const manyOptions = generateOptions(1e5); +manyOptions.push({ label: 'Banana', value: 'banana', description: 'A yellow fruit' }); + +const ManyOptionsStory: StoryFn = ({ numberOfOptions }) => { + const [value, setValue] = useState(manyOptions[5].value); + const options = useMemo(() => generateOptions(numberOfOptions), [numberOfOptions]); + return ( + { + setValue(val.value); + action('onChange')(val); + }} + /> + ); +}; + +export const ManyOptions: StoryObj = { + args: { + numberOfOptions: 1e5, + options: undefined, + value: undefined, + }, + render: ManyOptionsStory, +}; + export default meta; diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index ff83f3edc27..17e782a4b36 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -1,13 +1,17 @@ +import { css } from '@emotion/css'; +import { useVirtualizer } from '@tanstack/react-virtual'; import { useCombobox } from 'downshift'; -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; +import { useStyles2 } from '../../themes'; import { Icon } from '../Icon/Icon'; import { Input, Props as InputProps } from '../Input/Input'; -type Value = string | number; -type Option = { +export type Value = string | number; +export type Option = { label: string; value: Value; + description?: string; }; interface ComboboxProps @@ -33,32 +37,86 @@ function itemFilter(inputValue: string) { }; } +function estimateSize() { + return 60; +} + export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => { const [items, setItems] = useState(options); const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]); + const listRef = useRef(null); + + const styles = useStyles2(getStyles); + + const rowVirtualizer = useVirtualizer({ + count: items.length, + getScrollElement: () => listRef.current, + estimateSize, + overscan: 2, + }); const { getInputProps, getMenuProps, getItemProps, isOpen } = useCombobox({ items, itemToString, selectedItem, + scrollIntoView: () => {}, onInputValueChange: ({ inputValue }) => { setItems(options.filter(itemFilter(inputValue))); }, onSelectedItemChange: ({ selectedItem }) => onChange(selectedItem), + onHighlightedIndexChange: ({ highlightedIndex, type }) => { + if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) { + rowVirtualizer.scrollToIndex(highlightedIndex); + } + }, }); return (
} {...restProps} {...getInputProps()} /> -
    - {isOpen && - items.map((item, index) => { - return ( -
  • - {item.label} -
  • - ); - })} -
+
+ {isOpen && ( +
    + {rowVirtualizer.getVirtualItems().map((virtualRow) => { + return ( +
  • + {items[virtualRow.index].label} + {items[virtualRow.index].description && {items[virtualRow.index].description}} +
  • + ); + })} +
+ )} +
); }; + +const getStyles = () => ({ + dropdown: css({ + position: 'absolute', + height: 400, + width: 600, + overflowY: 'scroll', + contain: 'strict', + }), + menuItem: css({ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + display: 'flex', + flexDirection: 'column', + '&:first-child': { + fontWeight: 'bold', + }, + }), +}); diff --git a/yarn.lock b/yarn.lock index 7b3e11f200b..705fe14a62a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3684,6 +3684,7 @@ __metadata: "@storybook/react": "npm:^8.1.6" "@storybook/react-webpack5": "npm:^8.1.6" "@storybook/theming": "npm:^8.1.6" + "@tanstack/react-virtual": "npm:^3.5.1" "@testing-library/dom": "npm:10.0.0" "@testing-library/jest-dom": "npm:6.4.2" "@testing-library/react": "npm:15.0.2" @@ -7803,6 +7804,25 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-virtual@npm:^3.5.1": + version: 3.5.1 + resolution: "@tanstack/react-virtual@npm:3.5.1" + dependencies: + "@tanstack/virtual-core": "npm:3.5.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10/11c8e9e2391fa0c947848a720b7dccccb1e35a78ac3169d1c34629bbec4ec713eed78d4c17a3e540e01386ee25b600a53254357597ae91a5fe35c7436651e975 + languageName: node + linkType: hard + +"@tanstack/virtual-core@npm:3.5.1": + version: 3.5.1 + resolution: "@tanstack/virtual-core@npm:3.5.1" + checksum: 10/611ea09d37cf9183a51d2dfce401c3802b0d91f014e9bbaf32a6220ec7301b873b308130b795d935c0f5b73a43fd8358274915885da692d3e991eeeab6f8711b + languageName: node + linkType: hard + "@testing-library/dom@npm:10.0.0, @testing-library/dom@npm:>=7, @testing-library/dom@npm:^10.0.0": version: 10.0.0 resolution: "@testing-library/dom@npm:10.0.0" From 13e069b5aff3aeefb1ee1736c8f6df161074cb63 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Thu, 20 Jun 2024 09:52:31 -0400 Subject: [PATCH 051/122] Docs: Fix broken link (#89441) --- docs/sources/whatsnew/whats-new-in-v10-2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/whatsnew/whats-new-in-v10-2.md b/docs/sources/whatsnew/whats-new-in-v10-2.md index 02d1ddbbdf7..1db3a3d2505 100644 --- a/docs/sources/whatsnew/whats-new-in-v10-2.md +++ b/docs/sources/whatsnew/whats-new-in-v10-2.md @@ -379,7 +379,7 @@ _Generally available in all editions of Grafana_ Use the Grafana Alerting - Grafana OnCall integration to effortlessly connect alerts generated by Grafana Alerting with Grafana OnCall. From there, you can route them according to defined escalation chains and schedules. -To learn more, refer to the [Grafana OnCall integration for Alerting documentation](https://grafana.com/docs/grafana//alerting/alerting-rules/manage-contact-points/integrations/configure-oncall/), as well as the following video demo. +To learn more, refer to the [Grafana OnCall integration for Alerting documentation](https://grafana.com/docs/grafana//alerting/configure-notifications/manage-contact-points/integrations/configure-oncall/), as well as the following video demo. {{< youtube id="abRn5I61hxs?rel=0" >}} From 10b6011dd7870ebae3226e69ae046ed3f06d9779 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Thu, 20 Jun 2024 09:53:21 -0400 Subject: [PATCH 052/122] Docs: Edit mode updates (#89438) --- .../assess-dashboard-usage/index.md | 10 +- .../create-dashboard/index.md | 66 ++++++------- .../manage-dashboard-links/index.md | 98 +++++++++++-------- .../share-dashboards-panels/index.md | 34 ++++--- .../influxdb/query-editor/index.md | 6 +- .../getting-started/build-first-dashboard.md | 44 ++++----- .../configure-data-links/index.md | 4 +- .../configure-overrides/index.md | 5 +- .../configure-panel-options/index.md | 3 +- .../configure-thresholds/index.md | 2 + .../configure-value-mappings/index.md | 3 + .../panel-editor-overview/index.md | 21 ++-- 12 files changed, 161 insertions(+), 135 deletions(-) diff --git a/docs/sources/dashboards/assess-dashboard-usage/index.md b/docs/sources/dashboards/assess-dashboard-usage/index.md index 195e64b132c..1be93c317ed 100644 --- a/docs/sources/dashboards/assess-dashboard-usage/index.md +++ b/docs/sources/dashboards/assess-dashboard-usage/index.md @@ -50,9 +50,11 @@ refs: Usage insights enables you to have a better understanding of how your Grafana instance is used. -> **Note:** Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). -> Grafana Cloud insights logs include additional fields with their own dashboards. -> Read more in the [Grafana Cloud documentation](/docs/grafana-cloud/usage-insights/). +{{< admonition type="note" >}} +Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](https://grafana.com/docs/grafana-cloud/). +Grafana Cloud insights logs include additional fields with their own dashboards. +Read more in the [Grafana Cloud documentation](https://grafana.com/docs/grafana-cloud/account-management/usage-insights/). +{{< /admonition >}} The usage insights feature collects a number of aggregated data and stores them in the database: @@ -77,7 +79,7 @@ For every dashboard and data source, you can access usage information. To see dashboard usage information, click the dashboard insights icon in the header. -{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights.png" max-width="400px" class="docs-image--no-shadow" alt="Dashboard insights icon" >}} +{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-insights-11.2.png" max-width="400px" class="docs-image--no-shadow" alt="Dashboard insights icon" >}} Dashboard insights show the following information: diff --git a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md index bd4b2534295..f5ff0f8781d 100644 --- a/docs/sources/dashboards/build-dashboards/create-dashboard/index.md +++ b/docs/sources/dashboards/build-dashboards/create-dashboard/index.md @@ -19,22 +19,22 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//datasources/#special-data-sources - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//datasources/#special-data-sources + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/#special-data-sources visualization-specific-options: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/visualizations/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/visualizations/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/ configure-standard-options: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-standard-options/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-standard-options/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-standard-options/ configure-value-mappings: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-value-mappings/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-value-mappings/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-value-mappings/ generative-ai-features: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/manage-dashboards/#set-up-generative-ai-features-for-dashboards @@ -44,12 +44,12 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-thresholds/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-thresholds/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-thresholds/ data-sources: - pattern: /docs/grafana/ destination: /docs/grafana//datasources/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//datasources/ + destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/ add-a-data-source: - pattern: /docs/grafana/ destination: /docs/grafana//administration/data-source-management/#add-a-data-source @@ -69,17 +69,12 @@ refs: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-panel-options/#configure-repeating-panels - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-panel-options/#configure-repeating-panels + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-panel-options/#configure-repeating-panels override-field-values: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-overrides/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-overrides/ - dashboard: - - pattern: /docs/grafana/ - destination: /docs/grafana//datasources/#special-data-sources - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/#special-data-sources + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-overrides/ --- ## Create a dashboard @@ -95,7 +90,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee **To create a dashboard**: -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Click **New** and select **New Dashboard**. 1. On the empty dashboard, click **+ Add visualization**. @@ -104,7 +99,7 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee 1. In the dialog box that opens, do one of the following: - Select one of your existing data sources. - - Select one of the Grafana's [built-in special data sources](ref:built-in-special-data-sources). + - Select one of the Grafana [built-in special data sources](ref:built-in-special-data-sources). - Click **Configure a new data source** to set up a new one (Admins only). {{< figure class="float-right" src="/media/docs/grafana/dashboards/screenshot-data-source-selector-10.0.png" max-width="800px" alt="Select data source modal" >}} @@ -115,14 +110,10 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee For more information about data sources, refer to [Data sources](ref:data-sources) for specific guidelines. 1. Write or construct a query in the query language of your data source. - -1. Click the Refresh dashboard icon to query the data source. - - ![Refresh dashboard icon](/media/docs/grafana/dashboards/screenshot-refresh-dashboard-9.5.png) - +1. Click **Refresh** to query the data source. 1. In the visualization list, select a visualization type. - ![Visualization selector](/media/docs/grafana/dashboards/screenshot-select-visualization-9-5.png) + ![Visualization selector](/media/docs/grafana/dashboards/screenshot-select-visualization-11-2.png) Grafana displays a preview of your query results with the visualization applied. @@ -139,30 +130,35 @@ Dashboards and panels allow you to show your data in visual form. Each panel nee - [Configure thresholds](ref:configure-thresholds) - [Configure standard options](ref:configure-standard-options) -1. When you've finished editing your panel, click **Save** to save the dashboard. +1. When you've finished editing your panel, click **Save dashboard**. - Alternatively, click **Apply** if you want to see your changes applied to the dashboard first. Then click the save icon in the dashboard header. + Alternatively, click **Back to dashboard** if you want to see your changes applied to the dashboard first. Then click **Save dashboard** when you're ready. 1. Enter a title and description for your dashboard or have Grafana create them using [generative AI features](ref:generative-ai-features). 1. Select a folder, if applicable. 1. Click **Save**. -1. To add more panels to the dashboard, click **Add** in the dashboard header and select **Visualization** in the drop-down. +1. To add more panels to the dashboard, click **Back to dashboard**. + Then click **Add** in the dashboard header and select **Visualization** in the drop-down. ![Add drop-down](/media/docs/grafana/dashboards/screenshot-add-dropdown-10.0.png) When you add additional panels to the dashboard, you're taken straight to the **Edit panel** view. -## Copy an existing dashboard +1. When you've saved all the changes you want to make to the dashboard, click **Exit edit**. -To copy an existing dashboard, follow these steps: + Now, when you want to make more changes to the saved dashboard, click **Edit** in the top-right corner. -1. Click **Dashboards** in the primary menu. -1. Open the dashboard to be copied. -1. Click **Settings** (gear icon) in the top right of the dashboard. -1. Click **Save as** in the top-right corner of the dashboard. +## Copy a dashboard + +To copy a dashboard, follow these steps: + +1. Click **Dashboards** in the main menu. +1. Open the dashboard you want to copy. +1. Click **Edit** in top-right corner. +1. Click the **Save dashboard** drop-down and select **Save as copy**. 1. (Optional) Specify the name, folder, description, and whether or not to copy the original dashboard tags for the copied dashboard. - By default, the copied dashboard has the same name as the original dashboard with the word "Copy" appended and is located in the same folder. + By default, the copied dashboard has the same name as the original dashboard with the word "Copy" appended and is in the same folder. 1. Click **Save**. @@ -178,7 +174,7 @@ To see an example of repeating rows, refer to [Dashboard with repeating rows](ht **To configure repeating rows:** -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Navigate to the dashboard you want to work on. 1. At the top of the dashboard, click **Add** and select **Row** in the drop-down. @@ -192,7 +188,7 @@ To provide context to dashboard users, add the variable to the row title. ### Repeating rows and the Dashboard special data source -If a row includes panels using the special [Dashboard](ref:dashboard) data source—the data source that uses a result set from another panel in the same dashboard—then corresponding panels in repeated rows will reference the panel in the original row, not the ones in the repeated rows. +If a row includes panels using the special [Dashboard data source](ref:built-in-special-data-sources)—the data source that uses a result set from another panel in the same dashboard—then corresponding panels in repeated rows will reference the panel in the original row, not the ones in the repeated rows. For example, in a dashboard: @@ -205,7 +201,7 @@ For example, in a dashboard: You can place a panel on a dashboard in any location. -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Navigate to the dashboard you want to work on. 1. Click the panel title and drag the panel to the new location. @@ -213,6 +209,6 @@ You can place a panel on a dashboard in any location. You can size a dashboard panel to suits your needs. -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Navigate to the dashboard you want to work on. 1. To adjust the size of the panel, click and drag the lower-right corner of the panel. diff --git a/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md b/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md index 0960cceea09..94e75533736 100644 --- a/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md +++ b/docs/sources/dashboards/build-dashboards/manage-dashboard-links/index.md @@ -26,19 +26,19 @@ weight: 500 refs: data-links: - pattern: /docs/grafana/ - destination: /docs/grafana//panels-visualizations/configure-data-links/#data-links + destination: /docs/grafana//panels-visualizations/configure-data-links/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-data-links/#data-links + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-data-links/ data-link-variables: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/configure-data-links/#data-link-variables - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/configure-data-links/#data-link-variables + destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-data-links/#data-link-variables dashboard-url-variables: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/build-dashboards/create-dashboard-url-variables/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//dashboards/build-dashboards/create-dashboard-url-variables/ + destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/create-dashboard-url-variables/ --- # Manage dashboard links @@ -83,51 +83,64 @@ Once you've added a dashboard link, it appears in the upper right corner of your Add links to other dashboards at the top of your current dashboard. -1. While viewing the dashboard you want to link, click the gear at the top of the screen to open **Dashboard settings**. -1. Click **Links** and then click **Add Dashboard Link** or **New**. -1. In **Type**, select **dashboards**. -1. Select link options: - - **With tags** – Enter tags to limit the linked dashboards to only the ones with the tags you enter. Otherwise, Grafana includes links to all other dashboards. - - **As dropdown** – If you are linking to lots of dashboards, then you probably want to select this option and add an optional title to the dropdown. Otherwise, Grafana displays the dashboard links side by side across the top of your dashboard. - - **Time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now - - **Variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. For more information, see [Dashboard URL variables](ref:dashboard-url-variables). - - **Open in new tab** – Select this option if you want the dashboard link to open in a new tab or window. -1. Click **Add**. +1. In the dashboard you want to link, click **Edit**. +1. Click **Settings**. +1. Go to the **Links** tab and then click **Add dashboard link**. + + The default link type is **Dashboards**. + +1. In the **With tags** drop-down, enter tags to limit the linked dashboards to only the ones with the tags you enter. + + If you don't add any tags, Grafana includes links to all other dashboards. + +1. Set link options: + + - **Show as dropdown** – If you are linking to lots of dashboards, then you probably want to select this option and add an optional title to the dropdown. Otherwise, Grafana displays the dashboard links side by side across the top of your dashboard. + - **Include current time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now + - **Include current template variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. For more information, see [Dashboard URL variables](ref:dashboard-url-variables). + - **Open link in new tab** – Select this option if you want the dashboard link to open in a new tab or window. + +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Add a URL link to a dashboard Add a link to a URL at the top of your current dashboard. You can link to any available URL, including dashboards, panels, or external sites. You can even control the time range to ensure the user is zoomed in on the right data in Grafana. -1. While viewing the dashboard you want to link, click the gear at the top of the screen to open **Dashboard settings**. -1. Click **Links** and then click **Add Dashboard Link** or **New**. -1. In **Type**, select **link**. -1. Select link options: - - **Url** – Enter the URL you want to link to. Depending on the target, you might want to include field values. **Example:** https://github.com/grafana/grafana/issues/new?title=Dashboard%3A%20HTTP%20Requests - - **Title** – Enter the title you want the link to display. - - **Tooltip** – Enter the tooltip you want the link to display when the user hovers their mouse over it. - - **Icon** – Choose the icon you want displayed with the link. - - **Time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now - - `from` - Defines the lower limit of the time range, specified in ms epoch. - - `to` - Defines the upper limit of the time range, specified in ms epoch. - - `time` and `time.window` - Define a time range from `time-time.window/2` to `time+time.window/2`. Both params should be specified in ms. For example `?time=1500000000000&time.window=10000` will result in 10s time range from 1499999995000 to 1500000005000. - - **Variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. Here is the variable format: `https://${you-domain}/path/to/your/dashboard?var-${template-variable1}=value1&var-{template-variable2}=value2` **Example:** https://play.grafana.org/d/000000074/alerting?var-app=backend&var-server=backend_01&var-server=backend_03&var-interval=1h - - **Open in new tab** – Select this option if you want the dashboard link to open in a new tab or window. -1. Click **Add**. +1. In the dashboard you want to link, click **Edit**. +1. Click **Settings**. +1. Go to the **Links** tab and then click **Add dashboard link**. +1. In the **Type** drop-down, select **Link**. +1. In the **URL** field, enter the URL to which you want to link. + + Depending on the target, you might want to include field values. **Example:** https://github.com/grafana/grafana/issues/new?title=Dashboard%3A%20HTTP%20Requests + +1. In the **Tooltip** field, enter the tooltip you want the link to display when the user hovers their mouse over it. +1. In the **Icon** drop-down, choose the icon you want displayed with the link. +1. Set link options; by default, these options are enabled for URL links: + + - **Include current time range** – Select this option to include the dashboard time range in the link. When the user clicks the link, the linked dashboard opens with the indicated time range already set. **Example:** https://play.grafana.org/d/000000010/annotations?orgId=1&from=now-3h&to=now + - **Include current template variable values** – Select this option to include template variables currently used as query parameters in the link. When the user clicks the link, any matching templates in the linked dashboard are set to the values from the link. + - **Open link in new tab** – Select this option if you want the dashboard link to open in a new tab or window. + +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Update a dashboard link -To change or update an existing dashboard link, follow this procedure. +To change or update a dashboard link, follow this procedure. -1. In Dashboard Settings, on the Links tab, click the existing link that you want to edit. -1. Change the settings and then click **Update**. +1. In the dashboard settings, on the **Links** tab, click the link that you want to edit. +1. Change the settings and then click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. ## Duplicate a dashboard link -To duplicate an existing dashboard link, click the duplicate icon next to the existing link that you want to duplicate. +To duplicate a dashboard link, click the copy link icon next to the link that you want to duplicate. ### Delete a dashboard link -To delete an existing dashboard link, click the trash icon next to the duplicate icon that you want to delete. +To delete a dashboard link, click the red **X** next to the link that you want to delete and then **Delete**. ## Panel links @@ -144,7 +157,7 @@ Click the icon next to the panel title to see available panel links. To use a keyboard shortcut to open the panel, hover over the panel and press `e`. -1. Expand the **Panel options** section, scroll down to Panel links. +1. Expand the **Panel options** section, scroll down to **Panel links**. 1. Click **Add link**. 1. Enter a **Title**. **Title** is a human-readable label for the link that will be displayed in the UI. 1. Enter the **URL** you want to link to. @@ -152,9 +165,10 @@ Click the icon next to the panel title to see available panel links. - `from` - Defines the lower limit of the time range, specified in ms epoch. - `to` - Defines the upper limit of the time range, specified in ms epoch. - `time` and `time.window` - Define a time range from `time-time.window/2` to `time+time.window/2`. Both params should be specified in ms. For example `?time=1500000000000&time.window=10000` will result in 10s time range from 1499999995000 to 1500000005000. -1. If you want the link to open in a new tab, then select **Open in a new tab**. -1. Click **Save** to save changes and close the window. -1. Click **Save** in the upper right to save your changes to the dashboard. +1. If you want the link to open in a new tab, then select **Open in new tab**. +1. Click **Save** to save changes and close the dialog box. +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Update a panel link @@ -167,8 +181,9 @@ Click the icon next to the panel title to see available panel links. 1. Find the link that you want to make changes to. 1. Click the Edit (pencil) icon to open the Edit link window. 1. Make any necessary changes. -1. Click **Save** to save changes and close the window. -1. Click **Save** in the upper right to save your changes to the dashboard. +1. Click **Save** to save changes and close the dialog box. +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. ### Delete a panel link @@ -180,4 +195,5 @@ Click the icon next to the panel title to see available panel links. 1. Expand the **Panel options** section, scroll down to Panel links. 1. Find the link that you want to delete. 1. Click the **X** icon next to the link you want to delete. -1. Click **Save** in the upper right to save your changes to the dashboard. +1. Click **Save dashboard** in the top-right corner. +1. Click **Back to dashboard** and then **Exit edit**. diff --git a/docs/sources/dashboards/share-dashboards-panels/index.md b/docs/sources/dashboards/share-dashboards-panels/index.md index 5aacb8fde45..01ef05c1923 100644 --- a/docs/sources/dashboards/share-dashboards-panels/index.md +++ b/docs/sources/dashboards/share-dashboards-panels/index.md @@ -59,9 +59,9 @@ You must have an authorized viewer permission to see an image rendered by a dire The same permission is also required to view embedded links unless you have anonymous access permission enabled for your Grafana instance. -{{% admonition type="note" %}} +{{< admonition type="note" >}} As of Grafana 8.0, anonymous access permission is not available in Grafana Cloud. -{{% /admonition %}} +{{< /admonition >}} When you share a panel or dashboard as a snapshot, a snapshot (which is a panel or dashboard at the moment you take the snapshot) is publicly available on the web. Anyone with a link to it can access it. Because snapshots do not require any authorization to view, Grafana removes information related to the account it came from, as well as any sensitive data from the snapshot. @@ -69,13 +69,13 @@ When you share a panel or dashboard as a snapshot, a snapshot (which is a panel You can share a dashboard as a direct link or as a snapshot. You can also export a dashboard. -{{% admonition type="note" %}} +{{< admonition type="note" >}} If you change a dashboard, ensure that you save the changes before sharing. -{{% /admonition %}} +{{< /admonition >}} -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Click the dashboard you want to share. -1. Click the **Share** button at the top right of the screen. +1. Click **Share** in the top-right corner. The share dialog opens and shows the Link tab. @@ -108,7 +108,7 @@ If you created a snapshot by mistake, click **Delete snapshot** in the dialog bo To delete existing snapshots, follow these steps: -1. In the primary menu, click **Dashboards**. +1. Click **Dashboards** in the main menu. 1. Click **Snapshots** to go to the snapshots management page. 1. Click the red **x** next to the snapshot URL that you want to delete. @@ -120,7 +120,7 @@ The dashboard export action creates a Grafana JSON file that contains everything 1. Click **Dashboards** in the main menu. 1. Open the dashboard you want to export. -1. Click the **Share** icon in the top navigation bar. +1. Click **Share** in the top-right corner. 1. Click **Export**. If you're exporting the dashboard to use in another instance, with different data source UIDs, enable the **Export for sharing externally** switch. @@ -139,12 +139,14 @@ A template variable of the type `Constant` is automatically hidden in the dashbo You can generate and save PDF files of any dashboard. -> **Note:** Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). +{{< admonition type="note" >}} +Available in [Grafana Enterprise](ref:grafana-enterprise) and [Grafana Cloud](/docs/grafana-cloud/). +{{< /admonition >}} -1. Click **Dashboards** in the left-side menu. +1. Click **Dashboards** in the main menu. 1. Click the dashboard you want to share. -1. Click the **Share** button at the top right of the screen. -1. On the PDF tab, select a layout option for the exported dashboard: **Portrait** or **Landscape**. +1. Click **Share** in the top-right corner. +1. On the **PDF** tab, select a layout option for the exported dashboard: **Portrait** or **Landscape**. 1. Click **Save as PDF** to render the dashboard as a PDF file. Grafana opens the PDF file in a new window or browser tab. @@ -210,7 +212,7 @@ If you created a snapshot by mistake, click **Delete snapshot** in the dialog bo To delete existing snapshots, follow these steps: -1. In the primary menu, click **Dashboards**. +1. Click **Dashboards** in the main menu. 1. Click **Snapshots** to go to the snapshots management page. 1. Click the red **x** next to the snapshot URL that you want to delete. @@ -220,7 +222,9 @@ The snapshot is immediately deleted. You may need to clear your browser cache or You can embed a panel using an iframe on another web site. A viewer must be signed into Grafana to view the graph. -**> Note:** As of Grafana 8.0, anonymous access permission is no longer available for Grafana Cloud. +{{< admonition type="note" >}} +As of Grafana 8.0, anonymous access permission is no longer available for Grafana Cloud. +{{< /admonition >}} Here is an example of the HTML code: @@ -243,4 +247,4 @@ To create a library panel from the **Share Panel** dialog: 1. In **Library panel name**, enter the name. 1. In **Save in folder**, select the folder in which to save the library panel. By default, the root level is selected. 1. Click **Create library panel** to save your changes. -1. Save the dashboard. +1. Click **Save dashboard**. diff --git a/docs/sources/datasources/influxdb/query-editor/index.md b/docs/sources/datasources/influxdb/query-editor/index.md index 1c72886c783..76f1a50d733 100644 --- a/docs/sources/datasources/influxdb/query-editor/index.md +++ b/docs/sources/datasources/influxdb/query-editor/index.md @@ -19,16 +19,18 @@ refs: query-transform-data: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/query-transform-data/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/ panel-inspector: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/panel-inspector/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/panel-inspector/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/panel-inspector/ logs: - pattern: /docs/grafana/ destination: /docs/grafana//panels-visualizations/visualizations/logs/ - pattern: /docs/grafana-cloud/ - destination: /docs/grafana//panels-visualizations/visualizations/logs/ + destination: grafana-cloud/visualizations/panels-visualizations/visualizations/logs/ --- # InfluxDB query editor diff --git a/docs/sources/getting-started/build-first-dashboard.md b/docs/sources/getting-started/build-first-dashboard.md index 6b21909b9a5..0f1c4025b4c 100644 --- a/docs/sources/getting-started/build-first-dashboard.md +++ b/docs/sources/getting-started/build-first-dashboard.md @@ -14,7 +14,7 @@ weight: 200 # Build your first dashboard -This topic helps you get started with Grafana and build your first dashboard using the built-in `Grafana` data source. To learn more about Grafana, refer to [Introduction to Grafana]({{< relref "../introduction" >}}). +This topic helps you get started with Grafana and build your first dashboard using the built-in `Grafana` data source. To learn more about Grafana, refer to [Introduction to Grafana](https://grafana.com/docs/grafana//introduction/). {{% admonition type="note" %}} Grafana also offers a [free account with Grafana Cloud](/signup/cloud/connect-account?pg=gsdocs) to help getting started even easier and faster. You can install Grafana to self-host or get a free Grafana Cloud account. @@ -22,7 +22,7 @@ Grafana also offers a [free account with Grafana Cloud](/signup/cloud/connect-ac #### Install Grafana -Grafana can be installed on many different operating systems. For a list of the minimum hardware and software requirements, as well as instructions on installing Grafana, refer to [Install Grafana]({{< relref "../setup-grafana/installation" >}}). +Grafana can be installed on many different operating systems. For a list of the minimum hardware and software requirements, as well as instructions on installing Grafana, refer to [Install Grafana](https://grafana.com/docs/grafana//setup-grafana/installation/). #### Sign in to Grafana @@ -39,18 +39,18 @@ To sign in to Grafana for the first time: 1. Click **OK** on the prompt and change your password. -{{% admonition type="note" %}} +{{< admonition type="note" >}} We strongly recommend that you change the default administrator password. -{{% /admonition %}} +{{< /admonition >}} #### Create a dashboard -If you've already set up a data source that you know how to query, refer to [Create a dashboard]({{< relref "../dashboards/build-dashboards/create-dashboard" >}}) instead. +If you've already set up a data source that you know how to query, refer to [Create a dashboard](https://grafana.com/docs/grafana//dashboards/build-dashboards/create-dashboard/) instead. To create your first dashboard using the built-in `-- Grafana --` data source: -1. Click **Dashboards** in the left-side menu. -1. On the Dashboards page, click **New** and select **New Dashboard** from the drop-down menu. +1. Click **Dashboards** in the main menu. +1. On the **Dashboards** page, click **New** and select **New Dashboard** from the drop-down menu. 1. On the dashboard, click **+ Add visualization**. ![Empty dashboard state](/media/docs/grafana/dashboards/empty-dashboard-10.2.png) @@ -59,35 +59,33 @@ To create your first dashboard using the built-in `-- Grafana --` data source: {{< figure class="float-right" src="/media/docs/grafana/dashboards/screenshot-data-source-selector-10.0.png" max-width="800px" alt="Select data source dialog box" >}} - This configures your [query]({{< relref "../panels-visualizations/query-transform-data#add-a-query" >}}) and generates the Random Walk dashboard. + This configures your [query](https://grafana.com/docs/grafana//panels-visualizations/query-transform-data/#add-a-query) and generates the Random Walk dashboard. -1. Click the Refresh dashboard icon to query the data source. +1. Click **Refresh** to query the data source. +1. When you've finished editing your panel, click **Save dashboard**. - ![Refresh dashboard icon](/media/docs/grafana/dashboards/screenshot-refresh-dashboard-9.5.png) - -1. When you've finished editing your panel, click **Save** to save the dashboard. - - Alternatively, click **Apply** if you want to see your changes applied to the dashboard first. Then click the save icon in the dashboard header. + Alternatively, click **Back to dashboard** if you want to see your changes applied to the dashboard first. Then click **Save dashboard** when you're ready. 1. Add a descriptive title for the dashboard, or have Grafana create one using [generative AI features](https://grafana.com/docs/grafana//dashboards/manage-dashboards#set-up-generative-ai-features-for-dashboards), and then click **Save**. +1. Click **Back to dashboard** and then **Exit edit**. Congratulations, you have created your first dashboard and it's displaying results. #### Next steps -Continue to experiment with what you have built, try the [explore workflow]({{< relref "../explore" >}}) or another visualization feature. Refer to [Data sources]({{< relref "../datasources" >}}) for a list of supported data sources and instructions on how to [add a data source]({{< relref "../datasources#add-a-data-source" >}}). The following topics will be of interest to you: +Continue to experiment with what you have built, try the [explore workflow](https://grafana.com/docs/grafana//explore/) or another visualization feature. Refer to [Data sources](https://grafana.com/docs/grafana//datasources/) for a list of supported data sources and instructions on how to [add a data source](https://grafana.com/docs/grafana//datasources/#add-a-data-source). The following topics will be of interest to you: -- [Panels and visualizations]({{< relref "../panels-visualizations" >}}) -- [Dashboards]({{< relref "../dashboards" >}}) -- [Keyboard shortcuts]({{< relref "../dashboards/use-dashboards#keyboard-shortcuts" >}}) +- [Panels and visualizations](https://grafana.com/docs/grafana//panels-visualizations/) +- [Dashboards](https://grafana.com/docs/grafana//dashboards/) +- [Keyboard shortcuts](https://grafana.com/docs/grafana//dashboards/use-dashboards/#keyboard-shortcuts) - [Plugins](/grafana/plugins?orderBy=weight&direction=asc) ##### Admins The following topics are of interest to Grafana server admin users: -- [Grafana configuration]({{< relref "../setup-grafana/configure-grafana" >}}) -- [Authentication]({{< relref "../setup-grafana/configure-security/configure-authentication" >}}) -- [User permissions and roles]({{< relref "../administration/roles-and-permissions" >}}) -- [Provisioning]({{< relref "../administration/provisioning" >}}) -- [Grafana CLI]({{< relref "../cli" >}}) +- [Grafana configuration](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/) +- [Authentication](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-authentication/) +- [User permissions and roles](https://grafana.com/docs/grafana//administration/roles-and-permissions/) +- [Provisioning](https://grafana.com/docs/grafana//administration/provisioning/) +- [Grafana CLI](https://grafana.com/docs/grafana//cli/) diff --git a/docs/sources/panels-visualizations/configure-data-links/index.md b/docs/sources/panels-visualizations/configure-data-links/index.md index a6f644da36e..1b454b46848 100644 --- a/docs/sources/panels-visualizations/configure-data-links/index.md +++ b/docs/sources/panels-visualizations/configure-data-links/index.md @@ -250,5 +250,5 @@ If you want to add all of the current dashboard's variables to the URL, then use 1. If you want the link to open in a new tab, then toggle the **Open in a new tab** switch. 1. Click **Save** to save changes and close the dialog box. -1. Click **Apply** to see your changes in the dashboard. -1. Click the **Save dashboard** icon to save your changes to the dashboard. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. diff --git a/docs/sources/panels-visualizations/configure-overrides/index.md b/docs/sources/panels-visualizations/configure-overrides/index.md index c9ad3f7a815..2b7264ee6d4 100644 --- a/docs/sources/panels-visualizations/configure-overrides/index.md +++ b/docs/sources/panels-visualizations/configure-overrides/index.md @@ -244,7 +244,8 @@ To add a field override, follow these steps: 1. Select the field option that you want to apply. 1. Continue to add overrides to this field by clicking **Add override property**. 1. Add as many overrides as you need. -1. When you're finished, click **Save** to save all panel edits to the dashboard. +1. When you're finished, click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. ## Edit a field override @@ -259,5 +260,7 @@ To edit a field override, follow these steps: - Edit settings on existing overrides or field selection parameters. - Delete existing override properties by clicking the **X** next to the property. - Delete an override entirely by clicking the trash icon at the top-right corner. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. The changes you make take effect immediately. diff --git a/docs/sources/panels-visualizations/configure-panel-options/index.md b/docs/sources/panels-visualizations/configure-panel-options/index.md index aa80d4f5def..59c466a20fb 100644 --- a/docs/sources/panels-visualizations/configure-panel-options/index.md +++ b/docs/sources/panels-visualizations/configure-panel-options/index.md @@ -95,7 +95,8 @@ To configure repeating panels, follow these steps: - **Vertical** - Arrange panels in a column. The width of repeated panels is the same as the original, repeated panel. 1. If you selected **Horizontal** in the previous step, select a value in the **Max per row** drop-down list to control the maximum number of panels that can be in a row. -1. Click **Save**. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. 1. To propagate changes to all panels, reload the dashboard. You can stop a panel from repeating by selecting **Disable repeating** in the **Repeat by variable** drop-down list. diff --git a/docs/sources/panels-visualizations/configure-thresholds/index.md b/docs/sources/panels-visualizations/configure-thresholds/index.md index 5c5c9bb3707..102b8ea4505 100644 --- a/docs/sources/panels-visualizations/configure-thresholds/index.md +++ b/docs/sources/panels-visualizations/configure-thresholds/index.md @@ -174,6 +174,8 @@ You can add as many thresholds to a visualization as you want. Grafana automatic 1. Click the colored circle to the left of the threshold value to open the color picker, where you can update the threshold color. 1. Under **Thresholds mode**, select either **Absolute** or **Percentage**. 1. Under **Show thresholds**, set how the threshold is displayed or turn it off. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. To delete a threshold, navigate to the panel that contains the threshold and click the trash icon next to the threshold you want to remove. diff --git a/docs/sources/panels-visualizations/configure-value-mappings/index.md b/docs/sources/panels-visualizations/configure-value-mappings/index.md index 8b9f77276cb..754eeed29ee 100644 --- a/docs/sources/panels-visualizations/configure-value-mappings/index.md +++ b/docs/sources/panels-visualizations/configure-value-mappings/index.md @@ -185,3 +185,6 @@ The following image shows a table visualization with value mappings. If you want 1. Click **Update** to save the value mapping. After you've added a mapping, the **Edit value mappings** button replaces the **Add value mappings** button. Click the edit button to add or update mappings. + +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. diff --git a/docs/sources/panels-visualizations/panel-editor-overview/index.md b/docs/sources/panels-visualizations/panel-editor-overview/index.md index 38fba108702..4d131a900ca 100644 --- a/docs/sources/panels-visualizations/panel-editor-overview/index.md +++ b/docs/sources/panels-visualizations/panel-editor-overview/index.md @@ -52,7 +52,7 @@ refs: In the panel editor, you can update all the elements of a visualization including the data source, queries, time range, and visualization display options. -![Panel editor](/media/docs/grafana/panels-visualizations/screenshot-panel-editor-view.png) +![Panel editor](/media/docs/grafana/panels-visualizations/screenshot-grafana-11.2-panel-editor.png) This following sections describe the areas of the Grafana panel editor. @@ -60,26 +60,25 @@ This following sections describe the areas of the Grafana panel editor. The header section lists the dashboard in which the panel appears and the following controls: -- **Discard:** Discards changes you have made to the panel since you last saved the dashboard. -- **Save:** Saves changes you made to the panel. -- **Apply:** Applies changes you made and closes the panel editor, returning you to the dashboard. You'll have to save the dashboard to persist the applied changes. +- **Back to dashboard** - Return to the dashboard with changes applied, but not yet saved. +- **Discard panel changes** - Discard changes you have made to the panel since you last saved the dashboard. +- **Save dashboard** - Save your changes to the dashboard. ## Visualization preview The visualization preview section contains the following options: -- **Table view:** Convert any visualization to a table so you can see the data. Table views are helpful for troubleshooting. This view only contains the raw data. It doesn't include transformations you might have applied to the data or the formatting options available in the [Table](ref:table) visualization. -- **Fill:** The visualization preview fills the available space. If you change the width of the side pane or height of the bottom pane the visualization changes to fill the available space. -- **Actual:** The visualization preview has the exact size as the size on the dashboard. If not enough space is available, the visualization scales down preserving the aspect ratio. -- **Time range controls:** **Default** is either the browser local timezone or the timezone selected at a higher level. +- **Table view** - Convert any visualization to a table so you can see the data. Table views are helpful for troubleshooting. This view only contains the raw data. It doesn't include transformations you might have applied to the data or the formatting options available in the [Table](ref:table) visualization. +- **Time range controls** - **Default** is either the browser local timezone or the timezone selected at a higher level. +- **Refresh** - Query the data source. ## Data section The data section contains tabs where you enter queries, transform your data, and create alert rules (if applicable). -- **Query tab:** Select your data source and enter queries here. For more information, refer to [Add a query](ref:add-a-query). When you create a new dashboard, you'll be prompted to select a data source before you get to the panel editor. You set or update the data source in existing dashboards using the drop-down in the **Query** tab. -- **Transform tab:** Apply data transformations. For more information, refer to [Transform data](ref:transform-data). -- **Alert tab:** Write alert rules. For more information, refer to [the overview of Grafana Alerting](ref:the-overview-of-grafana-alerting). +- **Queries** - Select your data source and enter queries here. For more information, refer to [Add a query](ref:add-a-query). When you create a new dashboard, you'll be prompted to select a data source before you get to the panel editor. You set or update the data source in existing dashboards using the drop-down in the **Queries** tab. +- **Transformations** - Apply data transformations. For more information, refer to [Transform data](ref:transform-data). +- **Alert** - Write alert rules. For more information, refer to [the overview of Grafana Alerting](ref:the-overview-of-grafana-alerting). ## Panel display options From ffe755f95ddc7d4a701911f39347b8ca3f31827e Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:00:57 +0200 Subject: [PATCH 053/122] Alerting: Add warning in telegram contact point (#89397) * Add warning in telegram contact point * Protect possible undefined in settings * Address review comments --- .../integrations/configure-telegram.md | 5 +++++ .../components/receivers/form/ChannelSubForm.tsx | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md index e0c237211de..4b95b3c52ac 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md @@ -22,6 +22,11 @@ Use the Grafana Alerting - Telegram integration to send [Telegram](https://teleg ## Before you begin +### Telegram limitation + +Telegram messages are limited to 4096 UTF-8 characters. If you use a `parse_mode` other than `None`, truncation may result in an invalid message, causing the notification to fail. +For longer messages, we recommend using an alternative contact method. + ### Telegram bot API token and chat ID To integrate Grafana with Telegram, you need to obtain a Telegram **bot API token** and a **chat ID** (i.e., the ID of the Telegram chat where you want to receive the alert notifications). diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx index 14a5db7e24b..8a9050c6355 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelSubForm.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { sortBy } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useFormContext, FieldErrors, FieldValues, Controller } from 'react-hook-form'; +import { Controller, FieldErrors, FieldValues, useFormContext } from 'react-hook-form'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Alert, Button, Field, Select, useStyles2 } from '@grafana/ui'; @@ -53,6 +53,7 @@ export function ChannelSubForm({ const { control, watch, register, trigger, formState, setValue } = useFormContext(); const selectedType = watch(fieldName('type')) ?? defaultValues.type; // nope, setting "default" does not work at all. + const parse_mode = watch(fieldName('settings.parse_mode')); const { loading: testingReceiver } = useUnifiedAlertingSelector((state) => state.testReceivers); // TODO I don't like integration specific code here but other ways require a bigger refactoring @@ -122,13 +123,14 @@ export function ChannelSubForm({ }; const notifier = notifiers.find(({ dto: { type } }) => type === selectedType); + const isTelegram = selectedType === 'telegram'; + const isParseModeNone = parse_mode === 'None'; // if there are mandatory options defined, optional options will be hidden by a collapse // if there aren't mandatory options, all options will be shown without collapse const mandatoryOptions = notifier?.dto.options.filter((o) => o.required); const optionalOptions = notifier?.dto.options.filter((o) => !o.required); const contactPointTypeInputId = `contact-point-type-${pathPrefix}`; - return (
@@ -188,6 +190,14 @@ export function ChannelSubForm({
{notifier && (
+ {isTelegram && !isParseModeNone && ( + + )} defaultValues={defaultValues} selectedChannelOptions={mandatoryOptions?.length ? mandatoryOptions! : optionalOptions!} From c8f18023d38fcba42601091de5283c24a7390897 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 20 Jun 2024 17:11:56 +0300 Subject: [PATCH 054/122] Announcement banner: Add docs (#89455) * Announcement banner: Add docs * Add draft state --- .../announcement-banner/_index.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/sources/administration/announcement-banner/_index.md diff --git a/docs/sources/administration/announcement-banner/_index.md b/docs/sources/administration/announcement-banner/_index.md new file mode 100644 index 00000000000..14782fb77c2 --- /dev/null +++ b/docs/sources/administration/announcement-banner/_index.md @@ -0,0 +1,79 @@ +--- +draft: true +aliases: + - ../administration/reports/ + - ../enterprise/export-pdf/ + - ../enterprise/reporting/ + - ../panels/create-reports/ + - reporting/ +keywords: + - grafana + - announcement +labels: + products: + - cloud + - enterprise +menuTitle: Announcement banner +title: Create and configure announcement banner +description: Creat a banner to show important updates and information at the top of on every page +refs: + rbac: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/ +--- + +# Create and configure announcement banner + +Announcement banner allows you to show important updates and information at the top of every page in Grafana. You can use the announcement banner to communicate important information to your users, such as maintenance windows, new features, or other important updates. + +## Create or update an announcement banner + +Only organization administrators can create announcement banner by default. You can customize who can create announcement banner with [Role-based access control](ref:rbac). + +To create or update an announcement banner, follow these steps: + +1. Click **Administration > General > Announcement banner** in the side navigation menu. + + The Announcement banner page allows you to view, create and update the settings for a notification banner. Only one banner can be created at a time. + +2. Toggle the **Enable** switch on to enable the announcement banner. It can be toggled off at any time to disable the banner. + +3. Enter the **Message** for the announcement banner. + + The message field supports Markdown. To add a header, use the following syntax: + + ```markdown + ### Header + ``` + + To add a link, use the following syntax: + + ```markdown + [link text](https://www.example.com) + ``` + + The preview of the configured banner will appear on top of the form, under the **Preview** section. + +4. Select the banner's start date and time in the **Starts** field. + + By default, the banner starts being displayed immediately. You can set a future date and time for the banner to start displaying. + +5. Select the banner's end date and time in the **Ends** field. + + By default, the banner is displayed indefinitely. You can set a future date and time for the banner to stop displaying. + +6. Select the banner's visibility. + + **Everyone** - The banner is visible to all users, including on login page. + + **Authenticated users** - The banner is visible to only authenticated users. + +7. Select the type of banner in the **Variant** field. + + This will determine the color of the banner's background. + +8. Click **Save** to save the banner settings. + + The banner will now be displayed at the top of every page in Grafana. From fc4a9904c91c85f8b8fb21a0e45494252c253b5e Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Thu, 20 Jun 2024 15:22:50 +0100 Subject: [PATCH 055/122] Tempo: TraceQL metrics step option (#89434) * Add step option for metric queries * Add support for compare metric queries * Remove unneeded line * Delete step if it's not defined --- .../dataquery/x/TempoDataQuery_types.gen.ts | 4 ++++ .../kinds/dataquery/types_dataquery_gen.go | 3 +++ .../app/plugins/datasource/tempo/dataquery.cue | 2 ++ .../plugins/datasource/tempo/dataquery.gen.ts | 4 ++++ .../app/plugins/datasource/tempo/datasource.ts | 13 ++++++++++--- .../tempo/traceql/TempoQueryBuilderOptions.tsx | 17 +++++++++++++++++ 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index af517e367a2..6583afdedb2 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -58,6 +58,10 @@ export interface TempoQuery extends common.DataQuery { * Defines the maximum number of spans per spanset that are returned from Tempo */ spss?: number; + /** + * For metric queries, the step size to use + */ + step?: string; /** * The type of the table that is used to display the search results */ diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index bfb397b514d..4b3e3510266 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -129,6 +129,9 @@ type TempoQuery struct { // Defines the maximum number of spans per spanset that are returned from Tempo Spss *int64 `json:"spss,omitempty"` + // For metric queries, the step size to use + Step *string `json:"step,omitempty"` + // The type of the table that is used to display the search results TableType *SearchTableType `json:"tableType,omitempty"` } diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index b47078559b0..69dd0f653e2 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -51,6 +51,8 @@ composableKinds: DataQuery: { groupBy?: [...#TraceqlFilter] // The type of the table that is used to display the search results tableType?: #SearchTableType + // For metric queries, the step size to use + step?: string } @cuetsy(kind="interface") @grafana(TSVeneer="type") #TempoQueryType: "traceql" | "traceqlSearch" | "serviceMap" | "upload" | "nativeSearch" | "traceId" | "clear" @cuetsy(kind="type") diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index 5e0bb9543ee..7086fcc5c63 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -56,6 +56,10 @@ export interface TempoQuery extends common.DataQuery { * Defines the maximum number of spans per spanset that are returned from Tempo */ spss?: number; + /** + * For metric queries, the step size to use + */ + step?: string; /** * The type of the table that is used to display the search results */ diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index bcf330a29ec..b7ae4f1c77d 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -438,7 +438,7 @@ export class TempoDatasource extends DataSourceWithBackend, queryValue: string ): Observable => { - return this._request('/api/metrics/query_range', { + const requestData = { query: queryValue, start: options.range.from.unix(), end: options.range.to.unix(), - }).pipe( + step: options.targets[0].step, + }; + + if (!requestData.step) { + delete requestData.step; + } + + return this._request('/api/metrics/query_range', requestData).pipe( map((response) => { return { data: formatTraceQLMetrics(queryValue, response.data), diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index 98658945e54..d4e40701ffb 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -44,11 +44,15 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) const onTableTypeChange = (val: SearchTableType) => { onChange({ ...query, tableType: val }); }; + const onStepChange = (e: React.FormEvent) => { + onChange({ ...query, step: e.currentTarget.value }); + }; const collapsedInfoList = [ `Limit: ${query.limit || DEFAULT_LIMIT}`, `Spans Limit: ${query.spss || DEFAULT_SPSS}`, `Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`, + `Step: ${query.step || 'auto'}`, ]; return ( @@ -87,6 +91,19 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) onChange={onTableTypeChange} /> + + + From 27e800768ed82e83b621b36e0eed0f2345ea541e Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 20 Jun 2024 17:53:07 +0300 Subject: [PATCH 056/122] K8s: Improve identity mapping setup (#89450) --- pkg/api/dashboard_snapshot.go | 6 +- pkg/api/ds_query.go | 6 +- pkg/apiserver/builder/helper.go | 3 +- pkg/apiserver/endpoints/filters/requester.go | 67 +++++++++++++++++ pkg/infra/appcontext/user.go | 75 ++++++++----------- pkg/infra/appcontext/user_test.go | 8 +- pkg/registry/apis/dashboard/authorizer.go | 6 +- .../apis/dashboardsnapshot/sql_storage.go | 4 +- pkg/registry/apis/datasource/authorizer.go | 4 +- pkg/registry/apis/datasource/plugincontext.go | 4 +- pkg/registry/apis/datasource/querier.go | 4 +- pkg/registry/apis/example/register.go | 6 +- pkg/registry/apis/example/subresource.go | 6 +- pkg/registry/apis/folders/legacy_storage.go | 12 +-- pkg/registry/apis/folders/register.go | 6 +- pkg/registry/apis/folders/sub_access.go | 4 +- pkg/registry/apis/folders/sub_count.go | 4 +- .../auth/authenticator/signedinuser.go | 18 ++--- .../apiserver/auth/authorizer/org_id.go | 14 ++-- .../apiserver/auth/authorizer/org_role.go | 17 +++-- .../apiserver/auth/authorizer/stack_id.go | 6 +- .../apiserver/endpoints/request/namespace.go | 6 +- pkg/services/apiserver/service.go | 5 ++ pkg/services/datasources/service/legacy.go | 10 +-- pkg/services/libraryelements/accesscontrol.go | 4 +- .../plugincontext/plugincontext.go | 5 +- .../store/entity/grpc/authenticator.go | 17 +++-- .../entity/sqlstash/sql_storage_server.go | 10 +-- pkg/services/store/entity/sqlstash/utils.go | 4 +- pkg/services/store/resolver/ds_cache.go | 6 +- 30 files changed, 205 insertions(+), 142 deletions(-) create mode 100644 pkg/apiserver/endpoints/filters/requester.go diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 4e8acb6a3f3..544551fddda 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/apimachinery/identity" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/metrics" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -28,13 +28,13 @@ func (hs *HTTPServer) getCreatedSnapshotHandler() web.Handler { if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesSnapshots) { namespaceMapper := request.GetNamespaceMapper(hs.Cfg) return func(w http.ResponseWriter, r *http.Request) { - user, err := appcontext.User(r.Context()) + user, err := identity.GetRequester(r.Context()) if err != nil || user == nil { errhttp.Write(r.Context(), fmt.Errorf("no user"), w) return } r.URL.Path = "/apis/dashboardsnapshot.grafana.app/v0alpha1/namespaces/" + - namespaceMapper(user.OrgID) + "/dashboardsnapshots/create" + namespaceMapper(user.GetOrgID()) + "/dashboardsnapshots/create" hs.clientConfigProvider.DirectlyServeHTTP(w, r) } } diff --git a/pkg/api/ds_query.go b/pkg/api/ds_query.go index 77d02dc63fa..65f58ee81be 100644 --- a/pkg/api/ds_query.go +++ b/pkg/api/ds_query.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -43,12 +43,12 @@ func (hs *HTTPServer) getDSQueryEndpoint() web.Handler { // rewrite requests from /ds/query to the new query service namespaceMapper := request.GetNamespaceMapper(hs.Cfg) return func(w http.ResponseWriter, r *http.Request) { - user, err := appcontext.User(r.Context()) + user, err := identity.GetRequester(r.Context()) if err != nil || user == nil { errhttp.Write(r.Context(), fmt.Errorf("no user"), w) return } - r.URL.Path = "/apis/query.grafana.app/v0alpha1/namespaces/" + namespaceMapper(user.OrgID) + "/query" + r.URL.Path = "/apis/query.grafana.app/v0alpha1/namespaces/" + namespaceMapper(user.GetOrgID()) + "/query" hs.clientConfigProvider.DirectlyServeHTTP(w, r) } } diff --git a/pkg/apiserver/builder/helper.go b/pkg/apiserver/builder/helper.go index 195ff60f54a..8c0e84db2e2 100644 --- a/pkg/apiserver/builder/helper.go +++ b/pkg/apiserver/builder/helper.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" "golang.org/x/mod/semver" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -26,7 +27,6 @@ import ( "github.com/grafana/grafana/pkg/apiserver/endpoints/filters" "github.com/grafana/grafana/pkg/services/apiserver/options" - "github.com/prometheus/client_golang/prometheus" ) // TODO: this is a temporary hack to make rest.Connecter work with resource level routes @@ -96,6 +96,7 @@ func SetupConfig( // Needs to run last in request chain to function as expected, hence we register it first. handler := filters.WithTracingHTTPLoggingAttributes(requestHandler) + handler = filters.WithRequester(handler) handler = genericapiserver.DefaultBuildHandlerChain(handler, c) handler = filters.WithAcceptHeader(handler) handler = filters.WithPathRewriters(handler, pathRewriters) diff --git a/pkg/apiserver/endpoints/filters/requester.go b/pkg/apiserver/endpoints/filters/requester.go new file mode 100644 index 00000000000..eb88aa690bd --- /dev/null +++ b/pkg/apiserver/endpoints/filters/requester.go @@ -0,0 +1,67 @@ +package filters + +import ( + "net/http" + "slices" + + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/klog" + + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +// WithRequester makes sure there is an identity.Requester in context +func WithRequester(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + requester, err := identity.GetRequester(ctx) + if err != nil { + // Find the kubernetes user info + info, ok := request.UserFrom(ctx) + if ok { + if info.GetName() == user.Anonymous { + requester = &identity.StaticRequester{ + Namespace: identity.NamespaceAnonymous, + Name: info.GetName(), + Login: info.GetName(), + Permissions: map[int64]map[string][]string{}, + } + } + + if info.GetName() == user.APIServerUser || + slices.Contains(info.GetGroups(), user.SystemPrivilegedGroup) { + orgId := int64(1) + requester = &identity.StaticRequester{ + UserID: 1, + OrgID: orgId, + Name: info.GetName(), + Login: info.GetName(), + OrgRole: identity.RoleAdmin, + IsGrafanaAdmin: true, + Permissions: map[int64]map[string][]string{ + orgId: { + "*": {"*"}, // all resources, all scopes + + // Dashboards do not support wildcard action + // dashboards.ActionDashboardsRead: {"*"}, + // dashboards.ActionDashboardsCreate: {"*"}, + // dashboards.ActionDashboardsWrite: {"*"}, + // dashboards.ActionDashboardsDelete: {"*"}, + // dashboards.ActionFoldersCreate: {"*"}, + // dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, // access to read all folders + }, + }, + } + } + + if requester != nil { + req = req.WithContext(identity.WithRequester(ctx, requester)) + } else { + klog.V(5).Info("unable to map the k8s user to grafana requester", "user", info) + } + } + } + handler.ServeHTTP(w, req) + }) +} diff --git a/pkg/infra/appcontext/user.go b/pkg/infra/appcontext/user.go index c2a92da8d6a..d50eaacd8b9 100644 --- a/pkg/infra/appcontext/user.go +++ b/pkg/infra/appcontext/user.go @@ -4,13 +4,7 @@ import ( "context" "fmt" - k8suser "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/endpoints/request" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/dashboards" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" ) @@ -20,11 +14,16 @@ type ctxUserKey struct{} // WithUser adds the supplied SignedInUser to the context. func WithUser(ctx context.Context, usr *user.SignedInUser) context.Context { ctx = context.WithValue(ctx, ctxUserKey{}, usr) + // make sure it is also in the simplified version + if usr == nil || usr.IsNil() { + return identity.WithRequester(ctx, nil) + } return identity.WithRequester(ctx, usr) } // User extracts the SignedInUser from the supplied context. // Supports context set by appcontext.WithUser, gRPC server context, and HTTP ReqContext. +// Deprecated: use identity.GetRequester(ctx) when possible func User(ctx context.Context) (*user.SignedInUser, error) { // Set by appcontext.WithUser u, ok := ctx.Value(ctxUserKey{}).(*user.SignedInUser) @@ -38,44 +37,32 @@ func User(ctx context.Context) (*user.SignedInUser, error) { return grpcCtx.SignedInUser, nil } - // Set by incoming HTTP request - c, ok := ctxkey.Get(ctx).(*contextmodel.ReqContext) - if ok && c.SignedInUser != nil { - return c.SignedInUser, nil - } - - // Find the kubernetes user info - k8sUserInfo, ok := request.UserFrom(ctx) - if ok { - for _, group := range k8sUserInfo.GetGroups() { - switch group { - case k8suser.APIServerUser: - fallthrough - case k8suser.SystemPrivilegedGroup: - orgId := int64(1) - return &user.SignedInUser{ - UserID: 1, - OrgID: orgId, - Name: k8sUserInfo.GetName(), - Login: k8sUserInfo.GetName(), - OrgRole: identity.RoleAdmin, - IsGrafanaAdmin: true, - Permissions: map[int64]map[string][]string{ - orgId: { - "*": {"*"}, // all resources, all scopes - - // Dashboards do not support wildcard action - dashboards.ActionDashboardsRead: {"*"}, - dashboards.ActionDashboardsCreate: {"*"}, - dashboards.ActionDashboardsWrite: {"*"}, - dashboards.ActionDashboardsDelete: {"*"}, - dashboards.ActionFoldersCreate: {"*"}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, // access to read all folders - }, - }, - }, nil - } - } + // If the identity was set via requester, but not appcontext, we can map values + // NOTE: this path + requester, _ := identity.GetRequester(ctx) + if requester != nil { + id := requester.GetID() + userId, _ := id.UserID() + orgId := requester.GetOrgID() + return &user.SignedInUser{ + NamespacedID: id, + UserID: userId, + UserUID: requester.GetUID().ID(), + OrgID: orgId, + OrgName: requester.GetOrgName(), + OrgRole: requester.GetOrgRole(), + Login: requester.GetLogin(), + Email: requester.GetEmail(), + IsGrafanaAdmin: requester.GetIsGrafanaAdmin(), + Teams: requester.GetTeams(), + AuthID: requester.GetAuthID(), + AuthenticatedBy: requester.GetAuthenticatedBy(), + IDToken: requester.GetIDToken(), + Permissions: map[int64]map[string][]string{ + 0: requester.GetGlobalPermissions(), + orgId: requester.GetPermissions(), + }, + }, nil } return nil, fmt.Errorf("a SignedInUser was not found in the context") diff --git a/pkg/infra/appcontext/user_test.go b/pkg/infra/appcontext/user_test.go index 63300b13d17..55c9e0ac9a8 100644 --- a/pkg/infra/appcontext/user_test.go +++ b/pkg/infra/appcontext/user_test.go @@ -11,8 +11,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" grpccontext "github.com/grafana/grafana/pkg/services/grpcserver/context" "github.com/grafana/grafana/pkg/services/user" ) @@ -52,11 +50,9 @@ func TestUserFromContext(t *testing.T) { require.Equal(t, expected.UserID, actual.UserID) }) - t.Run("should return user set by HTTP ReqContext", func(t *testing.T) { + t.Run("should return user set as a requester", func(t *testing.T) { expected := testUser() - ctx := ctxkey.Set(context.Background(), &contextmodel.ReqContext{ - SignedInUser: expected, - }) + ctx := identity.WithRequester(context.Background(), expected) actual, err := appcontext.User(ctx) require.NoError(t, err) require.Equal(t, expected.UserID, actual.UserID) diff --git a/pkg/registry/apis/dashboard/authorizer.go b/pkg/registry/apis/dashboard/authorizer.go index 54a772591d5..f98c57d1383 100644 --- a/pkg/registry/apis/dashboard/authorizer.go +++ b/pkg/registry/apis/dashboard/authorizer.go @@ -5,8 +5,8 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" @@ -19,7 +19,7 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer { return authorizer.DecisionNoOpinion, "", nil } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "", err } @@ -27,7 +27,7 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer { if attr.GetName() == "" { // Discourage use of the "list" command for non super admin users if attr.GetVerb() == "list" && attr.GetResource() == v0alpha1.DashboardResourceInfo.GroupResource().Resource { - if !user.IsGrafanaAdmin { + if !user.GetIsGrafanaAdmin() { return authorizer.DecisionDeny, "list summary objects (or connect as GrafanaAdmin)", err } } diff --git a/pkg/registry/apis/dashboardsnapshot/sql_storage.go b/pkg/registry/apis/dashboardsnapshot/sql_storage.go index b21231a8613..26fdb8fff57 100644 --- a/pkg/registry/apis/dashboardsnapshot/sql_storage.go +++ b/pkg/registry/apis/dashboardsnapshot/sql_storage.go @@ -9,8 +9,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" ) @@ -73,7 +73,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/datasource/authorizer.go b/pkg/registry/apis/datasource/authorizer.go index 354325a65b1..570b438d808 100644 --- a/pkg/registry/apis/datasource/authorizer.go +++ b/pkg/registry/apis/datasource/authorizer.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" ) @@ -17,7 +17,7 @@ func (b *DataSourceAPIBuilder) GetAuthorizer() authorizer.Authorizer { if !attr.IsResourceRequest() { return authorizer.DecisionNoOpinion, "", nil } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "valid user is required", err } diff --git a/pkg/registry/apis/datasource/plugincontext.go b/pkg/registry/apis/datasource/plugincontext.go index 5871b45ab4c..aa9c01954da 100644 --- a/pkg/registry/apis/datasource/plugincontext.go +++ b/pkg/registry/apis/datasource/plugincontext.go @@ -7,9 +7,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" @@ -84,7 +84,7 @@ func (q *scopedDatasourceProvider) Get(ctx context.Context, uid string) (*v0alph if err != nil { return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/datasource/querier.go b/pkg/registry/apis/datasource/querier.go index 5811dbb8378..0a5806a4616 100644 --- a/pkg/registry/apis/datasource/querier.go +++ b/pkg/registry/apis/datasource/querier.go @@ -7,8 +7,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/datasources" @@ -108,7 +108,7 @@ func (q *DefaultQuerier) Datasource(ctx context.Context, name string) (*v0alpha1 if err != nil { return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go index edb2a90bbad..6dc5644bffa 100644 --- a/pkg/registry/apis/example/register.go +++ b/pkg/registry/apis/example/register.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/prometheus/client_golang/prometheus" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -20,12 +21,11 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/grafana/grafana/pkg/apimachinery/identity" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*TestingAPIBuilder)(nil) @@ -216,7 +216,7 @@ func (b *TestingAPIBuilder) GetAuthorizer() authorizer.Authorizer { } // require a user - _, err = appcontext.User(ctx) + _, err = identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "valid user is required", err } diff --git a/pkg/registry/apis/example/subresource.go b/pkg/registry/apis/example/subresource.go index 63fdcab5f02..3fa5807a9e9 100644 --- a/pkg/registry/apis/example/subresource.go +++ b/pkg/registry/apis/example/subresource.go @@ -8,8 +8,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" ) @@ -38,14 +38,14 @@ func (r *dummySubresourceREST) Connect(ctx context.Context, name string, opts ru return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } // This response object format is negotiated by k8s dummy := &example.DummySubresource{ - Info: fmt.Sprintf("%s/%s", info.Value, user.Login), + Info: fmt.Sprintf("%s/%s", info.Value, user.GetLogin()), } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index 5baedfdcefa..4802f2a2c86 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -11,9 +11,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/apiserver/storage/entity" "github.com/grafana/grafana/pkg/services/dashboards" @@ -83,7 +83,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -116,7 +116,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -146,7 +146,7 @@ func (s *legacyStorage) Create(ctx context.Context, return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -195,7 +195,7 @@ func (s *legacyStorage) Update(ctx context.Context, return nil, false, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, false, err } @@ -267,7 +267,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio if err != nil { return v, false, err // includes the not-found error } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, false, err } diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index d658c68db49..5b652dfb209 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/prometheus/client_golang/prometheus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -15,11 +16,11 @@ import ( common "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/builder" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" @@ -27,7 +28,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil) @@ -190,7 +190,7 @@ func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer { } // require a user - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, "valid user is required", err } diff --git a/pkg/registry/apis/folders/sub_access.go b/pkg/registry/apis/folders/sub_access.go index be506be9ece..3e7319f88a0 100644 --- a/pkg/registry/apis/folders/sub_access.go +++ b/pkg/registry/apis/folders/sub_access.go @@ -7,8 +7,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -49,7 +49,7 @@ func (r *subAccessREST) Connect(ctx context.Context, name string, opts runtime.O if err != nil { return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/registry/apis/folders/sub_count.go b/pkg/registry/apis/folders/sub_count.go index f3b7d7e61b1..9f89b6a933d 100644 --- a/pkg/registry/apis/folders/sub_count.go +++ b/pkg/registry/apis/folders/sub_count.go @@ -7,8 +7,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/folder" ) @@ -46,7 +46,7 @@ func (r *subCountREST) NewConnectOptions() (runtime.Object, bool, string) { } func (r *subCountREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/services/apiserver/auth/authenticator/signedinuser.go b/pkg/services/apiserver/auth/authenticator/signedinuser.go index fceecd02e8a..2c3e568f0e8 100644 --- a/pkg/services/apiserver/auth/authenticator/signedinuser.go +++ b/pkg/services/apiserver/auth/authenticator/signedinuser.go @@ -8,38 +8,38 @@ import ( k8suser "k8s.io/apiserver/pkg/authentication/user" "k8s.io/klog/v2" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" ) var _ authenticator.RequestFunc = signedInUserAuthenticator func signedInUserAuthenticator(req *http.Request) (*authenticator.Response, bool, error) { ctx := req.Context() - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { klog.V(5).Info("failed to get signed in user", "err", err) return nil, false, nil } userInfo := &k8suser.DefaultInfo{ - Name: signedInUser.Login, - UID: signedInUser.UserUID, + Name: signedInUser.GetLogin(), + UID: signedInUser.GetUID().ID(), Groups: []string{}, // In order to faithfully round-trip through an impersonation flow, Extra keys MUST be lowercase. // see: https://pkg.go.dev/k8s.io/apiserver@v0.27.1/pkg/authentication/user#Info Extra: map[string][]string{}, } - for _, v := range signedInUser.Teams { + for _, v := range signedInUser.GetTeams() { userInfo.Groups = append(userInfo.Groups, strconv.FormatInt(v, 10)) } // - if signedInUser.IDToken != "" { - userInfo.Extra["id-token"] = []string{signedInUser.IDToken} + if signedInUser.GetIDToken() != "" { + userInfo.Extra["id-token"] = []string{signedInUser.GetIDToken()} } - if signedInUser.OrgRole.IsValid() { - userInfo.Extra["user-instance-role"] = []string{string(signedInUser.OrgRole)} + if signedInUser.GetOrgRole().IsValid() { + userInfo.Extra["user-instance-role"] = []string{string(signedInUser.GetOrgRole())} } return &authenticator.Response{ diff --git a/pkg/services/apiserver/auth/authorizer/org_id.go b/pkg/services/apiserver/auth/authorizer/org_id.go index f73d3b59b32..f2630922280 100644 --- a/pkg/services/apiserver/auth/authorizer/org_id.go +++ b/pkg/services/apiserver/auth/authorizer/org_id.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" grafanarequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/org" @@ -27,7 +27,7 @@ func newOrgIDAuthorizer(orgService org.Service) *orgIDAuthorizer { } func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil } @@ -51,12 +51,16 @@ func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut } // Quick check that the same org is used - if signedInUser.OrgID == info.OrgID { + if signedInUser.GetOrgID() == info.OrgID { return authorizer.DecisionNoOpinion, "", nil } // Check if the user has access to the specified org - query := org.GetUserOrgListQuery{UserID: signedInUser.UserID} + userId, err := signedInUser.GetID().UserID() + if err != nil { + return authorizer.DecisionDeny, "unable to get userId", err + } + query := org.GetUserOrgListQuery{UserID: userId} result, err := auth.org.GetUserOrgList(ctx, &query) if err != nil { return authorizer.DecisionDeny, "error getting user org list", err @@ -68,5 +72,5 @@ func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut } } - return authorizer.DecisionDeny, fmt.Sprintf("user %d is not a member of org %d", signedInUser.UserID, info.OrgID), nil + return authorizer.DecisionDeny, fmt.Sprintf("user %d is not a member of org %d", userId, info.OrgID), nil } diff --git a/pkg/services/apiserver/auth/authorizer/org_role.go b/pkg/services/apiserver/auth/authorizer/org_role.go index 4717741a8d5..9c03f3ba795 100644 --- a/pkg/services/apiserver/auth/authorizer/org_role.go +++ b/pkg/services/apiserver/auth/authorizer/org_role.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" ) @@ -22,12 +22,13 @@ func newOrgRoleAuthorizer(orgService org.Service) *orgRoleAuthorizer { } func (auth orgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil } - switch signedInUser.OrgRole { + orgRole := signedInUser.GetOrgRole() + switch orgRole { case org.RoleAdmin: return authorizer.DecisionAllow, "", nil case org.RoleEditor: @@ -35,21 +36,21 @@ func (auth orgRoleAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib case "get", "list", "watch", "create", "update", "patch", "delete", "put", "post": return authorizer.DecisionAllow, "", nil default: - return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } case org.RoleViewer: switch a.GetVerb() { case "get", "list", "watch": return authorizer.DecisionAllow, "", nil default: - return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } case org.RoleNone: - return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(string(signedInUser.OrgRole), a), nil + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } return authorizer.DecisionDeny, "", nil } -func errorMessageForGrafanaOrgRole(grafanaOrgRole string, a authorizer.Attributes) string { - return fmt.Sprintf("Grafana org role (%s) didn't allow %s access on requested resource=%s, path=%s", grafanaOrgRole, a.GetVerb(), a.GetResource(), a.GetPath()) +func errorMessageForGrafanaOrgRole(orgRole identity.RoleType, a authorizer.Attributes) string { + return fmt.Sprintf("Grafana org role (%s) didn't allow %s access on requested resource=%s, path=%s", orgRole, a.GetVerb(), a.GetResource(), a.GetPath()) } diff --git a/pkg/services/apiserver/auth/authorizer/stack_id.go b/pkg/services/apiserver/auth/authorizer/stack_id.go index da63c25d5ba..af4c309e226 100644 --- a/pkg/services/apiserver/auth/authorizer/stack_id.go +++ b/pkg/services/apiserver/auth/authorizer/stack_id.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authorization/authorizer" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" grafanarequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/setting" @@ -27,7 +27,7 @@ func newStackIDAuthorizer(cfg *setting.Cfg) *stackIDAuthorizer { } func (auth stackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - signedInUser, err := appcontext.User(ctx) + signedInUser, err := identity.GetRequester(ctx) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil } @@ -48,7 +48,7 @@ func (auth stackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib if info.OrgID != 1 { return authorizer.DecisionDeny, "cloud instance requires org 1", nil } - if signedInUser.OrgID != 1 { + if signedInUser.GetOrgID() != 1 { return authorizer.DecisionDeny, "user must be in org 1", nil } diff --git a/pkg/services/apiserver/endpoints/request/namespace.go b/pkg/services/apiserver/endpoints/request/namespace.go index 899e342dc9f..2a4c768673b 100644 --- a/pkg/services/apiserver/endpoints/request/namespace.go +++ b/pkg/services/apiserver/endpoints/request/namespace.go @@ -8,7 +8,7 @@ import ( "k8s.io/apiserver/pkg/endpoints/request" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/setting" ) @@ -82,9 +82,9 @@ func ParseNamespace(ns string) (NamespaceInfo, error) { func OrgIDForList(ctx context.Context) (int64, error) { ns := request.NamespaceValue(ctx) if ns == "" { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if user != nil { - return user.OrgID, err + return user.GetOrgID(), err } return -1, err } diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 339b52bfd05..c50b5582430 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -157,6 +157,11 @@ func ProvideService( req.URL.Path = "/" } + if c.SignedInUser != nil { + ctx := appcontext.WithUser(req.Context(), c.SignedInUser) + req = req.WithContext(ctx) + } + resp := responsewriter.WrapForHTTP1Or2(c.Resp) s.handler.ServeHTTP(resp, req) } diff --git a/pkg/services/datasources/service/legacy.go b/pkg/services/datasources/service/legacy.go index db5b008b13e..b138a906cd6 100644 --- a/pkg/services/datasources/service/legacy.go +++ b/pkg/services/datasources/service/legacy.go @@ -8,7 +8,7 @@ import ( data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/datasources" ) @@ -56,11 +56,11 @@ func (s *cachingLegacyDataSourceLookup) GetDataSourceFromDeprecatedFields(ctx co if id == 0 && name == "" { return nil, fmt.Errorf("either name or ID must be set") } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } - key := fmt.Sprintf("%d/%s/%d", user.OrgID, name, id) + key := fmt.Sprintf("%d/%s/%d", user.GetOrgID(), name, id) s.cacheMu.Lock() defer s.cacheMu.Unlock() @@ -70,13 +70,13 @@ func (s *cachingLegacyDataSourceLookup) GetDataSourceFromDeprecatedFields(ctx co } ds, err := s.retriever.GetDataSource(ctx, &datasources.GetDataSourceQuery{ - OrgID: user.OrgID, + OrgID: user.GetOrgID(), Name: name, ID: id, }) if errors.Is(err, datasources.ErrDataSourceNotFound) && name != "" { ds, err = s.retriever.GetDataSource(ctx, &datasources.GetDataSourceQuery{ - OrgID: user.OrgID, + OrgID: user.GetOrgID(), UID: name, // Sometimes name is actually the UID :( }) } diff --git a/pkg/services/libraryelements/accesscontrol.go b/pkg/services/libraryelements/accesscontrol.go index 4760f9cf2ae..68d789cad06 100644 --- a/pkg/services/libraryelements/accesscontrol.go +++ b/pkg/services/libraryelements/accesscontrol.go @@ -5,7 +5,7 @@ import ( "errors" "strings" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" @@ -47,7 +47,7 @@ func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderSvc folder.Ser return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext.go b/pkg/services/pluginsintegration/plugincontext/plugincontext.go index 4d170d8867f..578e863c5d5 100644 --- a/pkg/services/pluginsintegration/plugincontext/plugincontext.go +++ b/pkg/services/pluginsintegration/plugincontext/plugincontext.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" @@ -95,7 +94,7 @@ func (p *Provider) GetWithDataSource(ctx context.Context, pluginID string, user } func (p *Provider) GetDataSourceInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return nil, err } @@ -115,7 +114,7 @@ func (p *Provider) PluginContextForDataSource(ctx context.Context, datasourceSet return backend.PluginContext{}, plugins.ErrPluginNotRegistered } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return backend.PluginContext{}, err } diff --git a/pkg/services/store/entity/grpc/authenticator.go b/pkg/services/store/entity/grpc/authenticator.go index c8e52721f92..5f0f3dd996d 100644 --- a/pkg/services/store/entity/grpc/authenticator.go +++ b/pkg/services/store/entity/grpc/authenticator.go @@ -5,11 +5,13 @@ import ( "fmt" "strconv" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/services/user" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" ) type Authenticator struct{} @@ -82,16 +84,17 @@ func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grp var _ grpc.StreamClientInterceptor = StreamClientInterceptor func WrapContext(ctx context.Context) (context.Context, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { return ctx, err } // set grpc metadata into the context to pass to the grpc server return metadata.NewOutgoingContext(ctx, metadata.Pairs( - "grafana-idtoken", user.IDToken, - "grafana-userid", strconv.FormatInt(user.UserID, 10), - "grafana-orgid", strconv.FormatInt(user.OrgID, 10), - "grafana-login", user.Login, + "grafana-idtoken", user.GetIDToken(), + "grafana-userid", user.GetID().ID(), + "grafana-useruid", user.GetUID().ID(), + "grafana-orgid", strconv.FormatInt(user.GetOrgID(), 10), + "grafana-login", user.GetLogin(), )), nil } diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 046ed69e4c7..a63bde240c4 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -17,7 +17,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" @@ -358,7 +358,7 @@ func (s *sqlEntityServer) History(ctx context.Context, r *entity.EntityHistoryRe return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return nil, err @@ -573,7 +573,7 @@ func (s *sqlEntityServer) List(ctx context.Context, r *entity.EntityListRequest) return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return nil, err @@ -803,7 +803,7 @@ func (s *sqlEntityServer) Watch(w entity.EntityStore_WatchServer) error { return err } - user, err := appcontext.User(w.Context()) + user, err := identity.GetRequester(w.Context()) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return err @@ -1289,7 +1289,7 @@ func (s *sqlEntityServer) FindReferences(ctx context.Context, r *entity.Referenc return nil, err } - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil { ctxLogger.Error("error getting user from ctx", "error", err) return nil, err diff --git a/pkg/services/store/entity/sqlstash/utils.go b/pkg/services/store/entity/sqlstash/utils.go index ade92f40374..3d4666b9649 100644 --- a/pkg/services/store/entity/sqlstash/utils.go +++ b/pkg/services/store/entity/sqlstash/utils.go @@ -8,7 +8,7 @@ import ( "fmt" "text/template" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/store/entity/db" "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate" ) @@ -27,7 +27,7 @@ func createETag(body []byte, meta []byte, status []byte) string { // getCurrentUser returns a string identifying the user making a request with // the given context. func getCurrentUser(ctx context.Context) (string, error) { - user, err := appcontext.User(ctx) + user, err := identity.GetRequester(ctx) if err != nil || user == nil { return "", fmt.Errorf("%w: %w", ErrUserNotFoundInContext, err) } diff --git a/pkg/services/store/resolver/ds_cache.go b/pkg/services/store/resolver/ds_cache.go index 74e945b8beb..f0be2ed28d5 100644 --- a/pkg/services/store/resolver/ds_cache.go +++ b/pkg/services/store/resolver/ds_cache.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/tsdb/grafanads" @@ -122,12 +122,12 @@ func (c *dsCache) getDS(ctx context.Context, uid string) (*dsVal, error) { } } - usr, err := appcontext.User(ctx) + usr, err := identity.GetRequester(ctx) if err != nil { return nil, nil // no user } - v, ok := c.cache[usr.OrgID] + v, ok := c.cache[usr.GetOrgID()] if !ok { return nil, nil // org not found } From c88de7f4d0734c13616a2b83357d28f82b9a6f36 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Thu, 20 Jun 2024 16:24:49 +0100 Subject: [PATCH 057/122] Alerting: Don't crash the page when trying to filter rules by regex (#89466) --- .../unified/hooks/useFilteredRules.test.ts | 22 +++++++++++++++++ .../unified/hooks/useFilteredRules.ts | 24 ++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts index b102a5b20c6..db277cea39d 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts @@ -256,4 +256,26 @@ describe('filterRules', function () { expect(filtered[0]?.groups[0]?.rules).toHaveLength(1); expect(filtered[0]?.groups[0]?.rules[0]?.name).toBe('CPU too high'); }); + + it('does not crash when trying to filter with regex-like strings', () => { + const rules = [mockCombinedRule({ name: '[alongnameinthefirstgroup]' })]; + + const ns = mockCombinedRuleNamespace({ + name: 'foo|bar', + groups: [ + // Create group with regex-like name so we can test that searching for it doesn't crash, + // and so we can test further paths of the filtering + // (we need some a group to be matched so we can test filtering by rule name as well) + mockCombinedRuleGroup('some|group', rules), + ], + }); + + const ruleQuery = '[alongnameinthefirstgroup][thishas spaces][somethingelse]'; + const namespaceQuery = 'foo|bar'; + const groupQuery = 'some|group'; + + const performFilter = () => + filterRules([ns], getFilter({ groupName: groupQuery, ruleName: ruleQuery, namespace: namespaceQuery })); + expect(performFilter).not.toThrow(); + }); }); diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index 3ceacb11b07..eaa13b43840 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -28,6 +28,19 @@ import { useURLSearchParams } from './useURLSearchParams'; const MAX_NEEDLE_SIZE = 25; const INFO_THRESHOLD = Infinity; +/** + * Escape query strings so that regex characters don't interfere + * with uFuzzy search methods. + * + * The fuzzy searching will take the query and generate a regex - but if the query + * contains a regex itself, then it can easily end up being split in a bad place + * and end up creating an invalid expression + */ +const escapeQueryRegex = (query: string) => { + // see https://stackoverflow.com/a/6969486 + return query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +}; + export function useRulesFilter() { const [queryParams, updateQueryParams] = useURLSearchParams(); const searchQuery = queryParams.get('search') ?? ''; @@ -130,10 +143,12 @@ export const filterRules = ( if (namespaceFilter) { const namespaceHaystack = filteredNamespaces.map((ns) => ns.name); + const escapedQuery = escapeQueryRegex(namespaceFilter); + const ufuzzy = getSearchInstance(namespaceFilter); const [idxs, info, order] = ufuzzy.search( namespaceHaystack, - namespaceFilter, + escapedQuery, getOutOfOrderLimit(namespaceFilter), INFO_THRESHOLD ); @@ -157,9 +172,11 @@ const reduceNamespaces = (filterState: RulesFilter) => { const groupsHaystack = filteredGroups.map((g) => g.name); const ufuzzy = getSearchInstance(groupNameFilter); + const escapedQuery = escapeQueryRegex(groupNameFilter); + const [idxs, info, order] = ufuzzy.search( groupsHaystack, - groupNameFilter, + escapedQuery, getOutOfOrderLimit(groupNameFilter), INFO_THRESHOLD ); @@ -193,10 +210,11 @@ const reduceGroups = (filterState: RulesFilter) => { if (ruleNameQuery) { const rulesHaystack = filteredRules.map((r) => r.name); const ufuzzy = getSearchInstance(ruleNameQuery); + const escapedQuery = escapeQueryRegex(ruleNameQuery); const [idxs, info, order] = ufuzzy.search( rulesHaystack, - ruleNameQuery, + escapedQuery, getOutOfOrderLimit(ruleNameQuery), INFO_THRESHOLD ); From 543e71eb2862187d12e8ee7742badb06e4c913e8 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 20 Jun 2024 18:49:19 +0300 Subject: [PATCH 058/122] Scopes: Pass selected scopes to dashboard JSON fetching (#89157) --- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 11 +++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 16 +++- .../DashboardScenePageStateManager.test.ts | 8 +- .../pages/DashboardScenePageStateManager.ts | 38 +++++++-- .../scene/Scopes/ScopesScene.test.tsx | 83 ++++++++++++------- .../scene/Scopes/testUtils.tsx | 40 +++++++++ .../features/dashboard/api/dashboard_api.ts | 15 +++- .../dashboard/services/DashboardLoaderSrv.ts | 2 +- .../features/dashboard/state/initDashboard.ts | 2 +- .../dashboard/utils/getScopesFromUrl.ts | 13 +++ 13 files changed, 193 insertions(+), 41 deletions(-) create mode 100644 public/app/features/dashboard/utils/getScopesFromUrl.ts diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8af56fc11c3..91d4bcf2a65 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -197,4 +197,5 @@ export interface FeatureToggles { failWrongDSUID?: boolean; databaseReadReplica?: boolean; zanzana?: boolean; + passScopeToDashboardApi?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3d763445ea2..2c2cdbd1763 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1344,6 +1344,17 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, + { + Name: "passScopeToDashboardApi", + Description: "Enables the passing of scopes to dashboards fetching in Grafana", + FrontendOnly: false, + Stage: FeatureStageExperimental, + Owner: grafanaDashboardsSquad, + RequiresRestart: false, + AllowSelfServe: false, + HideFromDocs: true, + HideFromAdminPage: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c0cf306fe54..d3552651a93 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -178,3 +178,4 @@ ssoSettingsLDAP,experimental,@grafana/identity-access-team,false,false,false failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false databaseReadReplica,experimental,@grafana/grafana-backend-services-squad,false,false,false zanzana,experimental,@grafana/identity-access-team,false,false,false +passScopeToDashboardApi,experimental,@grafana/dashboards-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 01c91f4b044..2ddff1bb681 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -722,4 +722,8 @@ const ( // FlagZanzana // Use openFGA as authorization engine. FlagZanzana = "zanzana" + + // FlagPassScopeToDashboardApi + // Enables the passing of scopes to dashboards fetching in Grafana + FlagPassScopeToDashboardApi = "passScopeToDashboardApi" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 4650fac0999..1535a47673d 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1678,6 +1678,20 @@ "requiresDevMode": true } }, + { + "metadata": { + "name": "passScopeToDashboardApi", + "resourceVersion": "1718290335877", + "creationTimestamp": "2024-06-13T14:52:15Z" + }, + "spec": { + "description": "Enables the passing of scopes to dashboards fetching in Grafana", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "pdfTables", @@ -2335,4 +2349,4 @@ } } ] -} \ No newline at end of file +} diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 8ae39b7b575..b67870e1b90 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -173,11 +173,11 @@ describe('DashboardScenePageStateManager', () => { const loader = new DashboardScenePageStateManager({}); - expect(loader.getFromCache('fake-dash')).toBeNull(); + expect(loader.getDashboardFromCache('fake-dash')).toBeNull(); await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); - expect(loader.getFromCache('fake-dash')).toBeDefined(); + expect(loader.getDashboardFromCache('fake-dash')).toBeDefined(); }); it('should load dashboard DTO from cache if requested again within 2s', async () => { @@ -186,7 +186,7 @@ describe('DashboardScenePageStateManager', () => { const loader = new DashboardScenePageStateManager({}); - expect(loader.getFromCache('fake-dash')).toBeNull(); + expect(loader.getDashboardFromCache('fake-dash')).toBeNull(); await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal }); expect(loadDashSpy).toHaveBeenCalledTimes(1); @@ -228,7 +228,7 @@ describe('DashboardScenePageStateManager', () => { keepDashboardFromExploreInLocalStorage: false, }); - expect(loader.getFromCache('fake-dash')).toBeNull(); + expect(loader.getDashboardFromCache('fake-dash')).toBeNull(); }); }); }); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 551d4208308..b55f281e4a8 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -16,6 +16,7 @@ import { import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking'; import { DashboardDTO, DashboardRoutes } from 'app/types'; +import { getScopesFromUrl } from '../../dashboard/utils/getScopesFromUrl'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { DashboardScene } from '../scene/DashboardScene'; import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel'; @@ -83,7 +84,7 @@ export class DashboardScenePageStateManager extends StateManagerBase ({ __esModule: true, ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => ({ - get: jest.fn().mockImplementation((url: string, params: { parent: string; scope: string[]; query?: string }) => { - if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_node_children')) { - return { - items: mocksNodes.filter( - ({ parent, spec: { title } }) => parent === params.parent && title.includes(params.query ?? '') - ), - }; - } - - if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/')) { - const name = url.replace('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/', ''); - - return mocksScopes.find((scope) => scope.metadata.name === name) ?? {}; - } - - if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_dashboard_bindings')) { - return { - items: mocksScopeDashboardBindings.filter(({ spec: { scope: bindingScope } }) => - params.scope.includes(bindingScope) - ), - }; - } - - return {}; - }), + get: getMock, }), })); @@ -109,6 +85,7 @@ describe('ScopesScene', () => { fetchScopeSpy.mockClear(); fetchSelectedScopesSpy.mockClear(); fetchSuggestedDashboardsSpy.mockClear(); + getMock.mockClear(); dashboardScene = buildTestScene(); scopesScene = dashboardScene.state.scopes!; @@ -418,4 +395,54 @@ describe('ScopesScene', () => { }); }); }); + + describe('Dashboards API', () => { + describe('Feature flag off', () => { + beforeAll(() => { + config.featureToggles.scopeFilters = true; + config.featureToggles.passScopeToDashboardApi = false; + }); + + beforeEach(() => { + setDashboardAPI(undefined); + locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3'); + }); + + it('Legacy API should not pass the scopes', () => { + config.featureToggles.kubernetesDashboards = false; + getDashboardAPI().getDashboardDTO('1'); + expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', undefined); + }); + + it('K8s API should not pass the scopes', () => { + config.featureToggles.kubernetesDashboards = true; + getDashboardAPI().getDashboardDTO('1'); + expect(getMock).toHaveBeenCalledWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1'); + }); + }); + + describe('Feature flag on', () => { + beforeAll(() => { + config.featureToggles.scopeFilters = true; + config.featureToggles.passScopeToDashboardApi = true; + }); + + beforeEach(() => { + setDashboardAPI(undefined); + locationService.push('/?scopes=scope1&scopes=scope2&scopes=scope3'); + }); + + it('Legacy API should pass the scopes', () => { + config.featureToggles.kubernetesDashboards = false; + getDashboardAPI().getDashboardDTO('1'); + expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', { scopes: ['scope1', 'scope2', 'scope3'] }); + }); + + it('K8s API should not pass the scopes', () => { + config.featureToggles.kubernetesDashboards = true; + getDashboardAPI().getDashboardDTO('1'); + expect(getMock).toHaveBeenCalledWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1'); + }); + }); + }); }); diff --git a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx index 7ca1fcaa8de..8e81fc1b162 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx @@ -252,6 +252,46 @@ export const fetchScopeSpy = jest.spyOn(api, 'fetchScope'); export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes'); export const fetchSuggestedDashboardsSpy = jest.spyOn(api, 'fetchSuggestedDashboards'); +export const getMock = jest + .fn() + .mockImplementation((url: string, params: { parent: string; scope: string[]; query?: string }) => { + if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_node_children')) { + return { + items: mocksNodes.filter( + ({ parent, spec: { title } }) => parent === params.parent && title.includes(params.query ?? '') + ), + }; + } + + if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/')) { + const name = url.replace('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/', ''); + + return mocksScopes.find((scope) => scope.metadata.name === name) ?? {}; + } + + if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_dashboard_bindings')) { + return { + items: mocksScopeDashboardBindings.filter(({ spec: { scope: bindingScope } }) => + params.scope.includes(bindingScope) + ), + }; + } + + if (url.startsWith('/api/dashboards/uid/')) { + return {}; + } + + if (url.startsWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/')) { + return { + metadata: { + name: '1', + }, + }; + } + + return {}; + }); + const selectors = { tree: { search: (nodeId: string) => `scopes-tree-${nodeId}-search`, diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 146349d8a10..f13a68102a3 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -6,6 +6,8 @@ import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types'; import { DashboardDTO, DashboardDataDTO } from 'app/types'; +import { getScopesFromUrl } from '../utils/getScopesFromUrl'; + export interface DashboardAPI { /** Get a dashboard with the access control metadata */ getDashboardDTO(uid: string): Promise; @@ -35,7 +37,11 @@ class LegacyDashboardAPI implements DashboardAPI { } getDashboardDTO(uid: string): Promise { - return getBackendSrv().get(`/api/dashboards/uid/${uid}`); + const scopesSearchParams = getScopesFromUrl(); + const scopes = scopesSearchParams?.getAll('scopes') ?? []; + const queryParams = scopes.length > 0 ? { scopes } : undefined; + + return getBackendSrv().get(`/api/dashboards/uid/${uid}`, queryParams); } } @@ -82,3 +88,10 @@ export function getDashboardAPI() { } return instance; } + +export function setDashboardAPI(override: DashboardAPI | undefined) { + if (process.env.NODE_ENV !== 'test') { + throw new Error('dashboardAPI can be only overridden in test environment'); + } + instance = override; +} diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index ae0ba93f42e..ba06819bc6c 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -77,7 +77,7 @@ export class DashboardLoaderSrv { }; }); } else if (uid) { - const cachedDashboard = stateManager.getFromCache(uid); + const cachedDashboard = stateManager.getDashboardFromCache(uid); if (cachedDashboard) { return Promise.resolve(cachedDashboard); } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 4d77b54728d..599c4ad2a8b 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -61,7 +61,7 @@ async function fetchDashboard( switch (args.routeName) { case DashboardRoutes.Home: { const stateManager = getDashboardScenePageStateManager(); - const cachedDashboard = stateManager.getFromCache(HOME_DASHBOARD_CACHE_KEY); + const cachedDashboard = stateManager.getDashboardFromCache(HOME_DASHBOARD_CACHE_KEY); if (cachedDashboard) { return cachedDashboard; diff --git a/public/app/features/dashboard/utils/getScopesFromUrl.ts b/public/app/features/dashboard/utils/getScopesFromUrl.ts new file mode 100644 index 00000000000..e9539037fe5 --- /dev/null +++ b/public/app/features/dashboard/utils/getScopesFromUrl.ts @@ -0,0 +1,13 @@ +import { config, locationService } from '@grafana/runtime'; + +export function getScopesFromUrl(): URLSearchParams | undefined { + if (!config.featureToggles.scopeFilters || !config.featureToggles.passScopeToDashboardApi) { + return undefined; + } + + const queryParams = locationService.getSearchObject(); + const rawScopes = queryParams['scopes'] ?? []; + const scopes = Array.isArray(rawScopes) ? rawScopes : [rawScopes]; + + return new URLSearchParams(scopes.map((scope) => ['scopes', String(scope)])); +} From 7c868e9b52d42fb012b26d6f40604f2c39d1fdef Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:18:11 +0200 Subject: [PATCH 059/122] RestoreDashboards: Create deletion process (#89234) * feat: add button * feat: add modal * refactor: move component * feat: add backend handling * fix: opening the modal * feat: adjust modal content * feat: adjust old delete modal * feat: add text to old delete modal --- .../api/browseDashboardsAPI.ts | 18 ++++++++ .../components/BrowseActions/DeleteModal.tsx | 12 +++++ .../components/PermanentlyDeleteModal.tsx | 44 +++++++++++++++++++ .../components/RecentlyDeletedActions.tsx | 35 +++++++++++++-- .../components/RestoreModal.tsx | 2 +- public/locales/en-US/grafana.json | 10 +++++ public/locales/pseudo-LOCALE/grafana.json | 10 +++++ 7 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/PermanentlyDeleteModal.tsx diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 49f91d2bcef..6a6e96e97ae 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -57,6 +57,10 @@ interface RestoreDashboardArgs { dashboardUID: string; } +interface HardDeleteDashboardArgs { + dashboardUID: string; +} + function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { async function backendSrvBaseQuery(requestOptions: RequestOptions) { try { @@ -382,6 +386,19 @@ export const browseDashboardsAPI = createApi({ method: 'PATCH', }), }), + + // permanently delete a dashboard. used in PermanentlyDeleteModal. + hardDeleteDashboard: builder.mutation({ + query: ({ dashboardUID }) => ({ + url: `/dashboards/uid/${dashboardUID}/trash`, + method: 'DELETE', + }), + onQueryStarted: ({ dashboardUID }, { queryFulfilled, dispatch }) => { + queryFulfilled.then(() => { + dispatch(refreshParents([dashboardUID])); + }); + }, + }), }), }); @@ -397,6 +414,7 @@ export const { useSaveDashboardMutation, useSaveFolderMutation, useRestoreDashboardMutation, + useHardDeleteDashboardMutation, } = browseDashboardsAPI; export { skipToken } from '@reduxjs/toolkit/query/react'; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx index a45d6446934..937da5d0924 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx @@ -35,6 +35,18 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P + {config.featureToggles.dashboardRestore && ( + <> + + + This action will delete the selected folders immediately but the selected dashboards will be marked + for deletion in 30 days. You can restore the dashboards anytime before the 30 days expires. Folders + cannot be restored. + + + + + )} This action will delete the following content: diff --git a/public/app/features/browse-dashboards/components/PermanentlyDeleteModal.tsx b/public/app/features/browse-dashboards/components/PermanentlyDeleteModal.tsx new file mode 100644 index 00000000000..0c0b89cb708 --- /dev/null +++ b/public/app/features/browse-dashboards/components/PermanentlyDeleteModal.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import { ConfirmModal, Text } from '@grafana/ui'; + +import { Trans, t } from '../../../core/internationalization'; + +import { Props as ModalProps } from './RestoreModal'; + +export const PermanentlyDeleteModal = ({ + onConfirm, + onDismiss, + selectedDashboards, + isLoading, + ...props +}: ModalProps) => { + const numberOfDashboards = selectedDashboards.length; + + const onDelete = async () => { + await onConfirm(); + onDismiss(); + }; + return ( + + + This action will delete {{ numberOfDashboards }} dashboards. + + + } + title={t('recently-deleted.permanently-delete-modal.title', 'Permanently Delete Dashboards')} + confirmationText={t('recently-deleted.permanently-delete-modal.confirm-text', 'Delete')} + confirmText={ + isLoading + ? t('recently-deleted.permanently-delete-modal.delete-loading', 'Deleting...') + : t('recently-deleted.permanently-delete-modal.delete-button', 'Delete') + } + confirmButtonVariant="destructive" + onConfirm={onDelete} + onDismiss={onDismiss} + {...props} + /> + ); +}; diff --git a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx index c1ceaa07289..b2f23879ab6 100644 --- a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx +++ b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx @@ -9,10 +9,12 @@ import appEvents from '../../../core/app_events'; import { Trans } from '../../../core/internationalization'; import { useDispatch } from '../../../types'; import { ShowModalReactEvent } from '../../../types/events'; -import { useRestoreDashboardMutation } from '../../browse-dashboards/api/browseDashboardsAPI'; -import { clearFolders, setAllSelection, useActionSelectionState } from '../../browse-dashboards/state'; +import { useHardDeleteDashboardMutation, useRestoreDashboardMutation } from '../api/browseDashboardsAPI'; import { useRecentlyDeletedStateManager } from '../api/useRecentlyDeletedStateManager'; -import { RestoreModal } from '../components/RestoreModal'; +import { clearFolders, setAllSelection, useActionSelectionState } from '../state'; + +import { PermanentlyDeleteModal } from './PermanentlyDeleteModal'; +import { RestoreModal } from './RestoreModal'; export function RecentlyDeletedActions() { const styles = useStyles2(getStyles); @@ -22,6 +24,7 @@ export function RecentlyDeletedActions() { const [, stateManager] = useRecentlyDeletedStateManager(); const [restoreDashboard, { isLoading: isRestoreLoading }] = useRestoreDashboardMutation(); + const [deleteDashboard, { isLoading: isDeleteLoading }] = useHardDeleteDashboardMutation(); const selectedDashboards = useMemo(() => { return Object.entries(selectedItemsState.dashboard) @@ -64,6 +67,13 @@ export function RecentlyDeletedActions() { onActionComplete(); }; + const onDelete = async () => { + const promises = selectedDashboards.map((uid) => deleteDashboard({ dashboardUID: uid })); + + await Promise.all(promises); + onActionComplete(); + }; + const showRestoreModal = () => { appEvents.publish( new ShowModalReactEvent({ @@ -77,17 +87,36 @@ export function RecentlyDeletedActions() { ); }; + const showDeleteModal = () => { + appEvents.publish( + new ShowModalReactEvent({ + component: PermanentlyDeleteModal, + props: { + selectedDashboards, + onConfirm: onDelete, + isLoading: isDeleteLoading, + }, + }) + ); + }; + return (
+
); } const getStyles = (theme: GrafanaTheme2) => ({ row: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(1), marginBottom: theme.spacing(2), }), }); diff --git a/public/app/features/browse-dashboards/components/RestoreModal.tsx b/public/app/features/browse-dashboards/components/RestoreModal.tsx index 520825861fb..9b16765b963 100644 --- a/public/app/features/browse-dashboards/components/RestoreModal.tsx +++ b/public/app/features/browse-dashboards/components/RestoreModal.tsx @@ -4,7 +4,7 @@ import { ConfirmModal, Text } from '@grafana/ui'; import { Trans, t } from '../../../core/internationalization'; -interface Props { +export interface Props { isOpen: boolean; onConfirm: () => Promise; onDismiss: () => void; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0f00b256579..2f81df6b661 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -78,6 +78,7 @@ "delete-button": "Delete", "delete-modal-invalid-text": "One or more folders contain library panels or alert rules. Delete these first in order to proceed.", "delete-modal-invalid-title": "Cannot delete folder", + "delete-modal-restore-dashboards-text": "This action will delete the selected folders immediately but the selected dashboards will be marked for deletion in 30 days. You can restore the dashboards anytime before the 30 days expires. Folders cannot be restored.", "delete-modal-text": "This action will delete the following content:", "delete-modal-title": "Delete", "deleting": "Deleting...", @@ -1574,8 +1575,17 @@ }, "recently-deleted": { "buttons": { + "delete": "Delete permanently", "restore": "Restore" }, + "permanently-delete-modal": { + "confirm-text": "Delete", + "delete-button": "Delete", + "delete-loading": "Deleting...", + "text_one": "This action will delete {{numberOfDashboards}} dashboards.", + "text_other": "This action will delete {{numberOfDashboards}} dashboards.", + "title": "Permanently Delete Dashboards" + }, "restore-modal": { "restore-button": "Restore", "restore-loading": "Restoring...", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 065ea9ab9b5..288b9f25abc 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -78,6 +78,7 @@ "delete-button": "Đęľęŧę", "delete-modal-invalid-text": "Øʼnę őř mőřę ƒőľđęřş čőʼnŧäįʼn ľįþřäřy päʼnęľş őř äľęřŧ řūľęş. Đęľęŧę ŧĥęşę ƒįřşŧ įʼn őřđęř ŧő přőčęęđ.", "delete-modal-invalid-title": "Cäʼnʼnőŧ đęľęŧę ƒőľđęř", + "delete-modal-restore-dashboards-text": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę ŧĥę şęľęčŧęđ ƒőľđęřş įmmęđįäŧęľy þūŧ ŧĥę şęľęčŧęđ đäşĥþőäřđş ŵįľľ þę mäřĸęđ ƒőř đęľęŧįőʼn įʼn 30 đäyş. Ÿőū čäʼn řęşŧőřę ŧĥę đäşĥþőäřđş äʼnyŧįmę þęƒőřę ŧĥę 30 đäyş ęχpįřęş. Főľđęřş čäʼnʼnőŧ þę řęşŧőřęđ.", "delete-modal-text": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę ŧĥę ƒőľľőŵįʼnģ čőʼnŧęʼnŧ:", "delete-modal-title": "Đęľęŧę", "deleting": "Đęľęŧįʼnģ...", @@ -1574,8 +1575,17 @@ }, "recently-deleted": { "buttons": { + "delete": "Đęľęŧę pęřmäʼnęʼnŧľy", "restore": "Ŗęşŧőřę" }, + "permanently-delete-modal": { + "confirm-text": "Đęľęŧę", + "delete-button": "Đęľęŧę", + "delete-loading": "Đęľęŧįʼnģ...", + "text_one": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę {{numberOfDashboards}} đäşĥþőäřđş.", + "text_other": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę {{numberOfDashboards}} đäşĥþőäřđş.", + "title": "Pęřmäʼnęʼnŧľy Đęľęŧę Đäşĥþőäřđş" + }, "restore-modal": { "restore-button": "Ŗęşŧőřę", "restore-loading": "Ŗęşŧőřįʼnģ...", From c2c30c25ed6ed258c23c8df17adcd5c0b2d51103 Mon Sep 17 00:00:00 2001 From: William Wernert Date: Thu, 20 Jun 2024 12:52:44 -0400 Subject: [PATCH 060/122] Notifications: Do not log email address in error message (#89314) Do not log email address in error message --- pkg/services/notifications/smtp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/notifications/smtp.go b/pkg/services/notifications/smtp.go index 2c46982b352..e0588ce3cb5 100644 --- a/pkg/services/notifications/smtp.go +++ b/pkg/services/notifications/smtp.go @@ -70,7 +70,7 @@ func (sc *SmtpClient) Send(ctx context.Context, messages ...*Message) (int, erro emailsSentFailed.Inc() } - err = fmt.Errorf("failed to send notification to email addresses: %s: %w", strings.Join(msg.To, ";"), innerError) + err = fmt.Errorf("failed to send email: %w", innerError) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) From 78e9226ad874ecdf4d257942f2ed230ec4751cde Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:15:09 -0500 Subject: [PATCH 061/122] CI: Add bump-version action to the release PR workflow (#89491) --- .github/workflows/bump-version.yml | 85 +++++++---------------- .github/workflows/release-pr.yml | 23 ++++-- go.mod | 42 ++++++----- go.sum | 64 +++++++++-------- go.work.sum | 12 +++- pkg/apiserver/go.mod | 1 + pkg/apiserver/go.sum | 1 + pkg/build/actions/bump-version/action.yml | 35 ++++++++++ pkg/build/actions/bump-version/main.go | 77 ++++++++++++++++++++ pkg/build/go.mod | 24 +++++-- pkg/build/go.sum | 49 +++++++++++-- pkg/util/xorm/go.mod | 2 +- pkg/util/xorm/go.sum | 3 +- 13 files changed, 291 insertions(+), 127 deletions(-) create mode 100644 pkg/build/actions/bump-version/action.yml create mode 100644 pkg/build/actions/bump-version/main.go diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 70717ef079a..5f0b7cf3020 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -5,74 +5,39 @@ on: version: description: 'Needs to match, exactly, the name of a milestone. The version to be released please respect: major.minor.patch, major.minor.patch-preview or major.minor.patch-preview format. example: 7.4.3, 7.4.3-preview or 7.4.3-preview1' required: true -env: - YARN_ENABLE_IMMUTABLE_INSTALLS: false + push: + default: true + required: false + dry_run: + default: false + required: false jobs: main: runs-on: ubuntu-latest steps: - # This is a basic workflow to help you get started with Actions - - uses: actions-ecosystem/action-regex-match@v2.0.2 - if: ${{ github.event.inputs.version != '' }} - id: regex-match - with: - text: ${{ github.event.inputs.version }} - regex: '^(\d+.\d+).\d+(?:-(?:(preview\d?)|(pre)))?$' - - uses: actions-ecosystem/action-regex-match@v2.0.2 - if: ${{ inputs.version_call != '' }} - id: regex-match-version-call - with: - text: ${{ inputs.version_call }} - regex: '^(\d+.\d+).\d+(?:-(?:(preview\d?)|(pre)))?$' - - name: Validate input version - if: ${{ steps.regex-match.outputs.match == '' && github.event.inputs.version != '' }} - run: | - echo "The input version format is not correct, please respect:\ - major.minor.patch, major.minor.patch-preview or major.minor.patch-preview format. \ - example: 7.4.3, 7.4.3-preview or 7.4.3-preview1" - exit 1 - - name: Validate input version call - if: ${{ inputs.version_call != '' && steps.regex-match-version-call.outputs.match == '' }} - run: | - echo "The input version format is not correct, please respect:\ - major.minor.patch, major.minor.patch-preview or major.minor.patch-preview format. \ - example: 7.4.3, 7.4.3-preview or 7.4.3-preview1" - exit 1 - - - uses: actions/checkout@v4 - - - name: Set intermedia variables - id: intermedia - run: | - echo "short_ref=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - echo "check_passed=false" >> $GITHUB_OUTPUT - echo "branch_name=v${{steps.regex-match.outputs.group1}}" >> $GITHUB_OUTPUT - echo "branch_exist=$(git ls-remote --heads https://github.com/grafana/grafana.git v${{ steps.regex-match.outputs.group1 }}.x | wc -l)" >> $GITHUB_OUTPUT - - - name: Checkout Actions + - name: Checkout Grafana uses: actions/checkout@v4 + - name: Update package.json versions + uses: ./pkg/build/actions/bump-version with: - repository: "grafana/grafana-github-actions" - path: ./actions - ref: main - # Go is required for also updating the schema versions as part of the precommit hook: - - uses: actions/setup-go@v4 - with: - go-version-file: go.mod - - uses: actions/setup-node@v4 - with: - node-version: '18' - - name: Install Actions - run: npm install --production --prefix ./actions - - name: "Generate token" + version: ${{ inputs.version }} + - if: ${{ inputs.push }} + name: Generate token id: generate_token uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 with: app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }} private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }} - - name: Run bump version (manually invoked) - uses: ./actions/bump-version - with: - token: ${{ steps.generate_token.outputs.token }} - metricsWriteAPIKey: ${{ secrets.GRAFANA_MISC_STATS_API_KEY }} - precommit_make_target: gen-cue + - if: ${{ inputs.push }} + name: Push & Create PR + run: | + git config --local user.name "github-actions[bot]" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local --add --bool push.autoSetupRemote true + git checkout -b "bump-version/${{ github.run_id }}/${{ inputs.version }}" + git add . + git commit -m "bump version ${{ inputs.version }}" + git push + gh pr create --dry-run=${{ inputs.dry_run }} -l "type/ci" -l "no-changelog" -B "${{ github.ref_name }}" --title "Release: Bump version to ${{ inputs.version }}" --body "Updated version to ${{ inputs.version }}" + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 6373612eca6..9b0db4be754 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -22,6 +22,7 @@ on: description: Branch to backport these changes to dry_run: required: false + default: false type: bool jobs: @@ -42,21 +43,31 @@ jobs: run: | git config --local user.name "github-actions[bot]" git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local --add --bool push.autoSetupRemote true - name: Create branch run: git checkout -b "release/${{ github.run_id }}/${{ inputs.version }}" - name: Generate changelog run: git commit --allow-empty -m "Update changelog placeholder" - - name: Update package.json - run: git commit --allow-empty -m "Update version in package.json(s) (and yarn install) placeholder" + - name: Update package.json versions + uses: ./pkg/build/actions/bump-version + with: + version: ${{ inputs.version }} + - name: add package.json changes + run: | + git add . + git commit -m "Update version to ${{ inputs.version }}" + - name: git push + if: ${{ inputs.dry_run }} != true + run: git push - name: Create PR without backports - if: "${{ github.event.inputs.backport == '' }}" + if: "${{ inputs.backport == '' }}" run: > - gh pr create --dry-run=${{ inputs.dry_run }} -H "release/${{ inputs.version }}" -B "${{ inputs.target }}" --title "Release: ${{ inputs.version }}" --body "These code changes must be merged after a release is complete" + gh pr create --dry-run=${{ inputs.dry_run }} -B "${{ inputs.target }}" --title "Release: ${{ inputs.version }}" --body "These code changes must be merged after a release is complete" env: GH_TOKEN: ${{ steps.generate_token.outputs.token }} - name: Create PR with backports - if: "${{ github.event.inputs.backport != '' }}" + if: "${{ inputs.backport != '' }}" run: > - gh pr create -l "backport ${{ inputs.backport }}" --dry-run=${{ inputs.dry_run }} -H "release/${{ inputs.version }}" -B "${{ inputs.target }}" --title "Release: ${{ inputs.version }}" --body "These code changes must be merged after a release is complete" + gh pr create -l "backport ${{ inputs.backport }}" -l "product-approved" --dry-run=${{ inputs.dry_run }} -B "${{ inputs.target }}" --title "Release: ${{ inputs.version }}" --body "These code changes must be merged after a release is complete" env: GH_TOKEN: ${{ steps.generate_token.outputs.token }} diff --git a/go.mod b/go.mod index a186b0dc4dd..b121c40ae80 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // @grafana/plugins-platform-backend github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // @grafana/grafana-backend-group github.com/alicebob/miniredis/v2 v2.30.1 // @grafana/alerting-backend - github.com/andybalholm/brotli v1.0.5 // @grafana/partner-datasources + github.com/andybalholm/brotli v1.0.6 // @grafana/partner-datasources github.com/apache/arrow/go/v15 v15.0.2 // @grafana/observability-metrics github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad github.com/aws/aws-sdk-go v1.51.31 // @grafana/aws-datasources @@ -122,9 +122,9 @@ require ( github.com/magefile/mage v1.15.0 // @grafana/grafana-release-guild github.com/matryer/is v1.4.0 // @grafana/grafana-as-code github.com/mattn/go-isatty v0.0.20 // @grafana/grafana-backend-group - github.com/mattn/go-sqlite3 v1.14.19 // @grafana/grafana-backend-group + github.com/mattn/go-sqlite3 v1.14.22 // @grafana/grafana-backend-group github.com/matttproud/golang_protobuf_extensions v1.0.4 // @grafana/alerting-backend - github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 // @grafana/grafana-bi-squad + github.com/microsoft/go-mssqldb v1.7.0 // @grafana/grafana-bi-squad github.com/mitchellh/mapstructure v1.5.0 //@grafana/identity-access-team github.com/modern-go/reflect2 v1.0.2 // @grafana/alerting-backend github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // @grafana/alerting-backend @@ -149,7 +149,7 @@ require ( github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // @grafana/grafana-backend-group github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // @grafana/grafana-backend-group github.com/urfave/cli v1.22.15 // indirect; @grafana/grafana-backend-group - github.com/urfave/cli/v2 v2.25.1 // @grafana/grafana-backend-group + github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group @@ -299,6 +299,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240620152449-c88de7f4d073 // @grafana/grafana-search-and-storage github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db // indirect github.com/grafana/sqlds/v3 v3.2.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect; @grafana/plugins-platform-backend @@ -329,7 +330,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/asmfmt v1.3.2 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect @@ -382,8 +382,8 @@ require ( github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/encoding v0.3.6 // indirect - github.com/sergi/go-diff v1.3.1 // indirect - github.com/shopspring/decimal v1.2.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect github.com/spf13/cast v1.6.0 // indirect @@ -411,7 +411,7 @@ require ( go.opentelemetry.io/otel/metric v1.26.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.0 // @grafana/identity-access-team golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect @@ -424,15 +424,11 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/kms v0.29.2 // indirect - lukechampine.com/uint128 v1.3.0 // indirect - modernc.org/cc/v3 v3.40.0 // indirect - modernc.org/ccgo/v3 v3.16.13 // indirect - modernc.org/libc v1.22.4 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/opt v0.1.3 // indirect - modernc.org/sqlite v1.21.2 // indirect - modernc.org/strutil v1.1.3 // indirect + modernc.org/libc v1.41.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/sqlite v1.29.6 // indirect + modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect @@ -440,21 +436,33 @@ require ( ) require ( + github.com/Masterminds/squirrel v1.5.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/karlseguin/ccache/v3 v3.0.5 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect github.com/natefinch/wrap v0.2.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pressly/goose/v3 v3.20.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sethvargo/go-retry v0.2.4 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/viper v1.18.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/mock v0.4.0 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect ) // Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream diff --git a/go.sum b/go.sum index 6d5235fff2a..2c66d93841d 100644 --- a/go.sum +++ b/go.sum @@ -1394,10 +1394,10 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4 v4.3.0/go.mod h1:Y/HgrePTmGy9HjdSGTqZNa+apUpTVIEVKXJyARP2lrk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU= github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk= @@ -1526,8 +1526,9 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGn github.com/alicebob/miniredis/v2 v2.30.1 h1:HM1rlQjq1bm9yQcsawJqSZBJ9AYgxvjkMsNtddh90+g= github.com/alicebob/miniredis/v2 v2.30.1/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= +github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= @@ -2333,6 +2334,8 @@ github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4 h1:t github.com/grafana/grafana/pkg/apiserver v0.0.0-20240226124929-648abdbd0ea4/go.mod h1:vpYI6DHvFO595rpQGooUjcyicjt9rOevldDdW79peV0= github.com/grafana/grafana/pkg/promlib v0.0.6 h1:FuRyHMIgVVXkLuJnCflNfk3gqJflmyiI+/ZuJ9MoAfY= github.com/grafana/grafana/pkg/promlib v0.0.6/go.mod h1:shFkrG1fQ/PPNRGhxAPNMLp0SAeG/jhqaLoG6n2191M= +github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240620152449-c88de7f4d073 h1:Au8+1QORZnMVo52+19dgkP4zzIlXyJ7x/d9ysSGQHOk= +github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20240620152449-c88de7f4d073/go.mod h1:zOInHv2y6bsgm9bIMsCVDaz1XylqIVX9r4amH4iuWPE= github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZyeetkAB8dmeti2o= github.com/grafana/grafana/pkg/util/xorm v0.0.1/go.mod h1:eNfbB9f2jM8o9RfwqwjY8SYm5tvowJ8Ly+iE4P9rXII= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -2540,7 +2543,6 @@ github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCM github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= @@ -2552,7 +2554,6 @@ github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiw github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= @@ -2623,7 +2624,6 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/karlseguin/ccache/v3 v3.0.5 h1:hFX25+fxzNjsRlREYsoGNa2LoVEw5mPF8wkWq/UnevQ= github.com/karlseguin/ccache/v3 v3.0.5/go.mod h1:qxC372+Qn+IBj8Pe3KvGjHPj0sWwEF7AeZVhsNPZ6uY= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -2750,8 +2750,8 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -2759,8 +2759,8 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQth github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246 h1:KT4vTYcHqj5C5hMK5kSpyAk7MnFqfHVWLL4VqMq66S8= -github.com/microsoft/go-mssqldb v1.6.1-0.20240214161942-b65008136246/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= +github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs= +github.com/microsoft/go-mssqldb v1.7.0/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -2868,6 +2868,8 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -3144,8 +3146,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCRwZQ= github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= @@ -3154,8 +3156,9 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v1.7.1/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= @@ -3289,8 +3292,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= -github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= -github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vburenin/ifacemaker v1.2.1/go.mod h1:5WqrzX2aD7/hi+okBjcaEQJMg4lDGrpuEX3B8L4Wgrs= @@ -4675,14 +4678,12 @@ k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSn k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= -modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= @@ -4693,11 +4694,12 @@ modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWs modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= -modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= -modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= @@ -4713,39 +4715,41 @@ modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= modernc.org/libc v1.21.2/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= -modernc.org/libc v1.22.4 h1:wymSbZb0AlrjdAVX3cjreCHTPCpPARbQXNz6BHPzdwQ= modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0= -modernc.org/sqlite v1.21.2 h1:ixuUG0QS413Vfzyx6FWx6PYTmHaOegTY+hjzhn7L+a0= modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0= +modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= +modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/tcl v1.13.2/go.mod h1:7CLiGIPo1M8Rv1Mitpv5akc2+8fxUd2y2UzC/MfMzy0= -modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws= modernc.org/tcl v1.15.1/go.mod h1:aEjeGJX2gz1oWKOLDVZ2tnEWLUrIn8H+GFu+akoDhqs= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/go.work.sum b/go.work.sum index d971095824b..03525eae44e 100644 --- a/go.work.sum +++ b/go.work.sum @@ -450,6 +450,7 @@ github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:m github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/OneOfOne/xxhash v1.2.6 h1:U68crOE3y3MPttCMQGywZOLrTeF5HHJ3/vDBCJn9/bA= github.com/OneOfOne/xxhash v1.2.6/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -468,6 +469,7 @@ github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSd github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= @@ -478,10 +480,13 @@ github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QL github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alexflint/go-arg v1.4.2/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk= +github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= @@ -517,6 +522,7 @@ github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= +github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bufbuild/protovalidate-go v0.2.1 h1:pJr07sYhliyfj/STAM7hU4J3FKpVeLVKvOBmOTN8j+s= github.com/bufbuild/protovalidate-go v0.2.1/go.mod h1:e7XXDtlxj5vlEyAgsrxpzayp4cEMKCSSb8ZCkin+MVA= @@ -1006,7 +1012,9 @@ github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgtype v1.10.0 h1:ILnBWrRMSXGczYvmkYD6PsYyVFUNLTnIUJHHDLmqk38= +github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx/v4 v4.15.0 h1:B7dTkXsdILD3MF987WGGCcg+tvLW6bZJdEcqVFeU//w= +github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1 h1:9Xm8CKtMZIXgcopfdWk/qZ1rt0HjMgfMR9nxxSeK6vk= github.com/jackspirou/syscerts v0.0.0-20160531025014-b68f5469dff1/go.mod h1:zuHl3Hh+e9P6gmBPvcqR1HjkaWHC/csgyskg6IaFKFo= github.com/jaegertracing/jaeger v1.41.0 h1:vVNky8dP46M2RjGaZ7qRENqylW+tBFay3h57N16Ip7M= @@ -1048,6 +1056,7 @@ github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIR github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= @@ -1085,6 +1094,7 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= @@ -1102,6 +1112,7 @@ github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/matryer/moq v0.3.1 h1:kLDiBJoGcusWS2BixGyTkF224aSCD8nLY24tj/NcTCs= github.com/matryer/moq v0.3.1/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= +github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= @@ -1714,7 +1725,6 @@ k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 29ef6ee029a..91fef83eb5c 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -15,6 +15,7 @@ require ( k8s.io/apiserver v0.29.2 k8s.io/client-go v0.29.3 k8s.io/component-base v0.29.2 + k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.120.1 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 ) diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 050f5aeac37..40b0a9143d6 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -440,6 +440,7 @@ k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ= k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/pkg/build/actions/bump-version/action.yml b/pkg/build/actions/bump-version/action.yml new file mode 100644 index 00000000000..c93d251a47a --- /dev/null +++ b/pkg/build/actions/bump-version/action.yml @@ -0,0 +1,35 @@ +name: Bump version +description: Updates package.json versions in Grafana and subpackages and regenerates schemas +inputs: + version: + description: The new version (ex. 1.2.3) + required: true +runs: + using: "composite" + steps: + - uses: actions-ecosystem/action-regex-match@v2.0.2 + if: ${{ github.event.inputs.version != '' }} + id: regex-match + with: + text: ${{ github.event.inputs.version }} + # https://semver.org/ + regex: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' + - name: Validate input version + if: ${{ steps.regex-match.outputs.match == '' && github.event.inputs.version != '' }} + shell: bash + run: | + echo "The input version format is not correct, please respect:\ + major.minor.patch, major.minor.patch-preview or major.minor.patch-preview format. \ + example: 7.4.3, 7.4.3-preview or 7.4.3-preview1" + exit 1 + - uses: actions/setup-go@v4 + with: + go-version-file: go.mod + - name: Bump versions + uses: dagger/dagger-for-github@v5 + with: + verb: run + args: go run ./pkg/build/actions/bump-version -version=${{ inputs.version }} + - name: make gen-cue + shell: bash + run: make gen-cue diff --git a/pkg/build/actions/bump-version/main.go b/pkg/build/actions/bump-version/main.go new file mode 100644 index 00000000000..2f419af558c --- /dev/null +++ b/pkg/build/actions/bump-version/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "path/filepath" + "strings" + + "dagger.io/dagger" +) + +func main() { + var ( + dir = flag.String("dir", ".", "The grafana directory") + version = flag.String("version", "", "Use the '-version' flag to manually set the version") + ctx = context.Background() + ) + flag.Parse() + if *version == "" { + log.Fatalln("-version must be set") + } + + d, err := dagger.Connect(ctx) + if err != nil { + panic(err) + } + + src := d.Host().Directory(filepath.Clean(*dir), dagger.HostDirectoryOpts{ + Include: []string{ + ".nvmrc", + ".yarn", + ".yarnrc.yml", + "nx.json", + "project.json", + "lerna.json", + "**/package.json", + "**/yarn.lock", + }, + }) + nodeVersion, err := NodeVersion(d, src).Stdout(ctx) + if err != nil { + log.Fatalln("error getting node version from '.nvmrc':", err) + } + + // Update version(s) + updated := WithUpdatedVersion(d, src, nodeVersion, *version) + log.Println("Exporting directory") + if _, err := updated.Export(ctx, filepath.Clean(*dir)); err != nil { + log.Fatalln("error exporting directory", err) + } + log.Println("Done exporting directory") +} + +// NodeVersion a container whose `stdout` will return the node version from the '.nvmrc' file in the directory 'src'. +func NodeVersion(d *dagger.Client, src *dagger.Directory) *dagger.Container { + return d.Container().From("alpine"). + WithMountedFile("/src/.nvmrc", src.File(".nvmrc")). + WithWorkdir("/src"). + WithExec([]string{"cat", ".nvmrc"}) +} + +func WithUpdatedVersion(d *dagger.Client, src *dagger.Directory, nodeVersion, version string) *dagger.Directory { + nodeVersion = strings.TrimPrefix(strings.TrimSpace(nodeVersion), "v") + image := fmt.Sprintf("node:%s-slim", nodeVersion) + + return d.Container().From(image). + WithDirectory("/src", src). + WithWorkdir("/src"). + WithExec([]string{"yarn", "install"}). + WithExec([]string{"npm", "version", version, "--no-git-tag-version"}). + WithExec([]string{"yarn", "run", "lerna", "version", version, "--no-push", "--no-git-tag-version", "--force-publish", "--exact", "--yes"}). + WithExec([]string{"yarn", "install"}). + Directory("/src"). + WithoutDirectory("node_modules") +} diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 91c7c5558c2..531adb6618e 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -35,7 +35,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect; @grafana/grafana-backend-group github.com/stretchr/testify v1.9.0 // @grafana/grafana-backend-group github.com/urfave/cli v1.22.15 // @grafana/grafana-backend-group - github.com/urfave/cli/v2 v2.25.1 // @grafana/grafana-backend-group + github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect; @grafana/plugins-platform-backend go.opentelemetry.io/otel v1.26.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.26.0 // indirect; @grafana/grafana-backend-group @@ -97,12 +97,28 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect ) +require dagger.io/dagger v0.11.8-rc.2 + require ( + github.com/99designs/gqlgen v0.17.44 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/Khan/genqlient v0.7.0 // indirect + github.com/adrg/xdg v0.4.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/term v0.5.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/sosodev/duration v1.2.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.11 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 // indirect + go.opentelemetry.io/otel/log v0.2.0-alpha // indirect + go.opentelemetry.io/otel/sdk/log v0.2.0-alpha // indirect + go.opentelemetry.io/proto/otlp v1.2.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/pkg/build/go.sum b/pkg/build/go.sum index f44c496bd24..4e40f12ad02 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -11,19 +11,29 @@ cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= +dagger.io/dagger v0.11.8-rc.2 h1:HCP3gXgAfJJBFitJm0jRdKWJsIKgSWNmVN9UV+CkOdk= +dagger.io/dagger v0.11.8-rc.2/go.mod h1:kIzxLfN8N8FXUCN9u5EHLBJUJMJm0t6XynecUzp0A5w= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= +github.com/99designs/gqlgen v0.17.44 h1:OS2wLk/67Y+vXM75XHbwRnNYJcbuJd4OBL76RX3NQQA= +github.com/99designs/gqlgen v0.17.44/go.mod h1:UTCu3xpK2mLI5qcMNw+HKDiEL77it/1XtAjisC4sLwM= github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w= +github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/aws/aws-sdk-go v1.51.31 h1:4TM+sNc+Dzs7wY1sJ0+J8i60c6rkgnKP1pvPx8ghsSY= github.com/aws/aws-sdk-go v1.51.31/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -32,6 +42,8 @@ github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTS github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8= github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -42,7 +54,6 @@ github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -130,6 +141,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksP github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -142,7 +155,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -150,6 +162,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/moby v23.0.4+incompatible h1:A/pe8vi9KIKhNbzR0G3wW4ACKDsMgXILBveMqiJNa8M= github.com/moby/moby v23.0.4+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -161,7 +175,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -169,12 +182,15 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us= +github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -190,8 +206,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= -github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= -github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= +github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -204,14 +222,31 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc= go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha h1:z2s6Zba+OUyayRv5m1AXWNUTGh57K1iMhy6emU5QT5Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha/go.mod h1:paOXXyUgPW6jYxYkP0pB47H2zHE1fPvMJ4E4G9LHOi0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38= +go.opentelemetry.io/otel/log v0.2.0-alpha h1:ixOPvMzserpqA07SENHvRzkZOsnG0XbPr74hv1AQ+n0= +go.opentelemetry.io/otel/log v0.2.0-alpha/go.mod h1:vbFZc65yq4c4ssvXY43y/nIqkNJLxORrqw0L85P59LA= go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/sdk/log v0.2.0-alpha h1:jGTkL/jroJ31jnP6jDl34N/mDOfRGGYZHcHsCM+5kWA= +go.opentelemetry.io/otel/sdk/log v0.2.0-alpha/go.mod h1:Hd8Lw9FPGUM3pfY7iGMRvFaC2Nyau4Ajb5WnQ9OdIho= go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +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-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -219,6 +254,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= 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= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -254,6 +290,7 @@ 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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/pkg/util/xorm/go.mod b/pkg/util/xorm/go.mod index e981f997db3..45b2e78666a 100644 --- a/pkg/util/xorm/go.mod +++ b/pkg/util/xorm/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/util/xorm go 1.21.10 require ( - github.com/mattn/go-sqlite3 v1.14.19 + github.com/mattn/go-sqlite3 v1.14.22 github.com/stretchr/testify v1.9.0 xorm.io/builder v0.3.6 xorm.io/core v0.7.3 diff --git a/pkg/util/xorm/go.sum b/pkg/util/xorm/go.sum index c003657520e..35ee55d83fe 100644 --- a/pkg/util/xorm/go.sum +++ b/pkg/util/xorm/go.sum @@ -17,8 +17,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= From 880c180424003fd126372c5177d2ea37a2ff45ea Mon Sep 17 00:00:00 2001 From: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:30:28 -0500 Subject: [PATCH 062/122] Docs: Fixed broken links on Prometheus data source page (#89485) --- docs/sources/datasources/prometheus/_index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/datasources/prometheus/_index.md b/docs/sources/datasources/prometheus/_index.md index 94895b902c3..e5fa61ef1d6 100644 --- a/docs/sources/datasources/prometheus/_index.md +++ b/docs/sources/datasources/prometheus/_index.md @@ -89,7 +89,7 @@ For instructions on how to add a data source to Grafana, refer to the [administr Only users with the organization `administrator` role can add data sources and edit existing data sources. Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system. -Once you've added the Prometheus data source, you can [configure it](ref:configure-prometheus-data-source) so that your Grafana instance's users can create queries in its [query editor]({{< relref "./query-editor" >}}) when they [build dashboards](ref:build-dashboards), use [Explore](ref:explore), and [annotate visualizations][annotate visualizations]. +Once you've added the Prometheus data source, you can [configure it](ref:configure-prometheus-data-source) so that your Grafana instance's users can create queries in its [query editor]({{< relref "./query-editor" >}}) when they [build dashboards](ref:build-dashboards), use [Explore](ref:explore), and [annotate visualizations](ref:annotate-visualizations). The following guides will help you get started with the Prometheus data source: @@ -152,7 +152,7 @@ We also bundle a dashboard within Grafana so you can start viewing your metrics **To import the bundled dashboard:** -1. Navigate to the data source's [configuration page](#configure-the-data-source). +1. Navigate to the data source's [configuration page](ref:configure-prometheus-data-source). 1. Select the **Dashboards** tab. This displays dashboards for Grafana and Prometheus. From d988f5c3b064fade6e96511e0024190c22d48e50 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 20 Jun 2024 14:45:39 -0400 Subject: [PATCH 063/122] fix: sql instrumentation dual registration error (#89508) fix dual registration error --- pkg/services/sqlstore/database_wrapper.go | 21 +++++++++++++++++++++ pkg/services/sqlstore/replstore.go | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/database_wrapper.go b/pkg/services/sqlstore/database_wrapper.go index fab3664cd1c..7cafa1f712b 100644 --- a/pkg/services/sqlstore/database_wrapper.go +++ b/pkg/services/sqlstore/database_wrapper.go @@ -58,6 +58,27 @@ func WrapDatabaseDriverWithHooks(dbType string, tracer tracing.Tracer) string { return driverWithHooks } +// WrapDatabaseDriverWithHooks creates a fake database driver that +// executes pre and post functions which we use to gather metrics about +// database queries. It also registers the metrics. +func WrapDatabaseReplDriverWithHooks(dbType string, tracer tracing.Tracer) string { + drivers := map[string]driver.Driver{ + migrator.SQLite: &sqlite3.SQLiteDriver{}, + migrator.MySQL: &mysql.MySQLDriver{}, + migrator.Postgres: &pq.Driver{}, + } + + d, exist := drivers[dbType] + if !exist { + return dbType + } + + driverWithHooks := dbType + "ReplicaWithHooks" + sql.Register(driverWithHooks, sqlhooks.Wrap(d, &databaseQueryWrapper{log: log.New("sqlstore.metrics"), tracer: tracer})) + core.RegisterDriver(driverWithHooks, &databaseQueryWrapperDriver{dbType: dbType}) + return driverWithHooks +} + // databaseQueryWrapper satisfies the sqlhook.databaseQueryWrapper interface // which allow us to wrap all SQL queries with a `Before` & `After` hook. type databaseQueryWrapper struct { diff --git a/pkg/services/sqlstore/replstore.go b/pkg/services/sqlstore/replstore.go index c3540d79c4f..050341a3606 100644 --- a/pkg/services/sqlstore/replstore.go +++ b/pkg/services/sqlstore/replstore.go @@ -118,7 +118,7 @@ func (ss *SQLStore) initReadOnlyEngine(engine *xorm.Engine) error { ss.dbCfg = dbCfg if ss.cfg.DatabaseInstrumentQueries { - ss.dbCfg.Type = WrapDatabaseDriverWithHooks(ss.dbCfg.Type, ss.tracer) + ss.dbCfg.Type = WrapDatabaseReplDriverWithHooks(ss.dbCfg.Type, ss.tracer) } if engine == nil { From 5e95c1bdf86d4b30212a85ec8c1494901106f53f Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 20 Jun 2024 22:32:19 +0300 Subject: [PATCH 064/122] Storage: Move grpc helper from entity store to resource store (#89490) --- go.work.sum | 19 ++ pkg/apiserver/endpoints/filters/requester.go | 2 +- pkg/apiserver/go.mod | 1 - pkg/apiserver/go.sum | 1 - pkg/services/authz/client.go | 5 +- pkg/services/store/entity/client_wrapper.go | 2 +- .../store/entity/grpc/authenticator.go | 100 ----------- pkg/services/store/entity/server/service.go | 7 +- pkg/storage/unified/resource/go.mod | 16 +- pkg/storage/unified/resource/go.sum | 12 ++ .../unified/resource/grpc/authenticator.go | 163 ++++++++++++++++++ .../resource/grpc/authenticator_test.go | 34 ++++ 12 files changed, 250 insertions(+), 112 deletions(-) delete mode 100644 pkg/services/store/entity/grpc/authenticator.go create mode 100644 pkg/storage/unified/resource/grpc/authenticator.go create mode 100644 pkg/storage/unified/resource/grpc/authenticator_test.go diff --git a/go.work.sum b/go.work.sum index 03525eae44e..1a0cde64319 100644 --- a/go.work.sum +++ b/go.work.sum @@ -412,6 +412,8 @@ github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWV github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw= +github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v3 v3.0.0 h1:1PwO5w5VCtlUUl+KTOBsTGZlhjWkcybsGaAau52tOy8= @@ -778,6 +780,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9 github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= +github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ= +github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -820,6 +824,8 @@ github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY= github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= @@ -1028,6 +1034,7 @@ github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo= github.com/jmattheis/goverter v1.4.0 h1:SrboBYMpGkj1XSgFhWwqzdP024zIa1+58YzUm+0jcBE= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= @@ -1091,6 +1098,7 @@ github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0ot github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= @@ -1248,6 +1256,7 @@ github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfP github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= +github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= @@ -1397,6 +1406,7 @@ github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYm github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tursodatabase/libsql-client-go v0.0.0-20240411070317-a1138d155304/go.mod h1:2Fu26tjM011BLeR5+jwTfs6DX/fNMEWV/3CBZvggrA4= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -1417,6 +1427,7 @@ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk= +github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d h1:3wDi6J5APMqaHBVPuVd7RmHD2gRTfqbdcVSpCNoUWtk= github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= @@ -1456,6 +1467,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/ydb-platform/ydb-go-genproto v0.0.0-20240126124512-dbb0e1720dbf/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I= +github.com/ydb-platform/ydb-go-sdk/v3 v3.55.1/go.mod h1:udNPW8eupyH/EZocecFmaSNJacKKYjzQa7cVgX5U2nc= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= @@ -1469,6 +1482,7 @@ github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY3 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -1691,6 +1705,7 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= @@ -1725,6 +1740,7 @@ k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= @@ -1739,7 +1755,10 @@ k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= diff --git a/pkg/apiserver/endpoints/filters/requester.go b/pkg/apiserver/endpoints/filters/requester.go index eb88aa690bd..f148c6d252b 100644 --- a/pkg/apiserver/endpoints/filters/requester.go +++ b/pkg/apiserver/endpoints/filters/requester.go @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/klog" + "k8s.io/klog/v2" "github.com/grafana/grafana/pkg/apimachinery/identity" ) diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 91fef83eb5c..29ef6ee029a 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -15,7 +15,6 @@ require ( k8s.io/apiserver v0.29.2 k8s.io/client-go v0.29.3 k8s.io/component-base v0.29.2 - k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.120.1 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 ) diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 40b0a9143d6..050f5aeac37 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -440,7 +440,6 @@ k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ= k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/pkg/services/authz/client.go b/pkg/services/authz/client.go index be0328aa284..aaf6ecfc248 100644 --- a/pkg/services/authz/client.go +++ b/pkg/services/authz/client.go @@ -5,18 +5,17 @@ import ( "github.com/fullstorydev/grpchan" "github.com/fullstorydev/grpchan/inprocgrpc" + authzv1 "github.com/grafana/authlib/authz/proto/v1" grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - authzv1 "github.com/grafana/authlib/authz/proto/v1" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/grpcserver" - grpcUtils "github.com/grafana/grafana/pkg/services/store/entity/grpc" "github.com/grafana/grafana/pkg/setting" + grpcUtils "github.com/grafana/grafana/pkg/storage/unified/resource/grpc" ) type Client interface { diff --git a/pkg/services/store/entity/client_wrapper.go b/pkg/services/store/entity/client_wrapper.go index a4b7572d7a2..6b488d6d5e7 100644 --- a/pkg/services/store/entity/client_wrapper.go +++ b/pkg/services/store/entity/client_wrapper.go @@ -6,7 +6,7 @@ import ( grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" "google.golang.org/grpc" - grpcUtils "github.com/grafana/grafana/pkg/services/store/entity/grpc" + grpcUtils "github.com/grafana/grafana/pkg/storage/unified/resource/grpc" ) func NewEntityStoreClientLocal(server EntityStoreServer) EntityStoreClient { diff --git a/pkg/services/store/entity/grpc/authenticator.go b/pkg/services/store/entity/grpc/authenticator.go deleted file mode 100644 index 5f0f3dd996d..00000000000 --- a/pkg/services/store/entity/grpc/authenticator.go +++ /dev/null @@ -1,100 +0,0 @@ -package grpc - -import ( - "context" - "fmt" - "strconv" - - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/infra/appcontext" - "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" - "github.com/grafana/grafana/pkg/services/user" -) - -type Authenticator struct{} - -func (f *Authenticator) Authenticate(ctx context.Context) (context.Context, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return nil, fmt.Errorf("no metadata found") - } - - // TODO: use id token instead of these fields - login := md.Get("grafana-login")[0] - if login == "" { - return nil, fmt.Errorf("no login found in context") - } - userID, err := strconv.ParseInt(md.Get("grafana-userid")[0], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid user id: %w", err) - } - orgID, err := strconv.ParseInt(md.Get("grafana-orgid")[0], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid org id: %w", err) - } - - // TODO: validate id token - /* - idToken := md.Get("grafana-idtoken")[0] - if idToken == "" { - return nil, fmt.Errorf("no id token found in context") - } - jwtToken, err := jwt.ParseSigned(idToken) - if err != nil { - return nil, fmt.Errorf("invalid id token: %w", err) - } - claims := jwt.Claims{} - err = jwtToken.UnsafeClaimsWithoutVerification(&claims) - if err != nil { - return nil, fmt.Errorf("invalid id token: %w", err) - } - // fmt.Printf("JWT CLAIMS: %+v\n", claims) - */ - - return appcontext.WithUser(ctx, &user.SignedInUser{ - Login: login, - UserID: userID, - OrgID: orgID, - }), nil -} - -var _ interceptors.Authenticator = (*Authenticator)(nil) - -func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - ctx, err := WrapContext(ctx) - if err != nil { - return err - } - return invoker(ctx, method, req, reply, cc, opts...) -} - -var _ grpc.UnaryClientInterceptor = UnaryClientInterceptor - -func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { - ctx, err := WrapContext(ctx) - if err != nil { - return nil, err - } - return streamer(ctx, desc, cc, method, opts...) -} - -var _ grpc.StreamClientInterceptor = StreamClientInterceptor - -func WrapContext(ctx context.Context) (context.Context, error) { - user, err := identity.GetRequester(ctx) - if err != nil { - return ctx, err - } - - // set grpc metadata into the context to pass to the grpc server - return metadata.NewOutgoingContext(ctx, metadata.Pairs( - "grafana-idtoken", user.GetIDToken(), - "grafana-userid", user.GetID().ID(), - "grafana-useruid", user.GetUID().ID(), - "grafana-orgid", strconv.FormatInt(user.GetOrgID(), 10), - "grafana-login", user.GetLogin(), - )), nil -} diff --git a/pkg/services/store/entity/server/service.go b/pkg/services/store/entity/server/service.go index 89a150ccb39..7c2d6652c18 100644 --- a/pkg/services/store/entity/server/service.go +++ b/pkg/services/store/entity/server/service.go @@ -4,6 +4,9 @@ import ( "context" "github.com/grafana/dskit/services" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc/health/grpc_health_v1" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/modules" @@ -13,11 +16,9 @@ import ( "github.com/grafana/grafana/pkg/services/grpcserver/interceptors" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/entity/db/dbimpl" - "github.com/grafana/grafana/pkg/services/store/entity/grpc" "github.com/grafana/grafana/pkg/services/store/entity/sqlstash" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" - "google.golang.org/grpc/health/grpc_health_v1" + "github.com/grafana/grafana/pkg/storage/unified/resource/grpc" ) var ( diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 2ebcdfc5fed..52164669af4 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -2,13 +2,25 @@ module github.com/grafana/grafana/pkg/storage/unified/resource go 1.21.10 -require github.com/stretchr/testify v1.9.0 +require ( + github.com/grafana/authlib v0.0.0-20240611075137-331cbe4e840f + github.com/stretchr/testify v1.9.0 + google.golang.org/grpc v1.64.0 +) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 4d56934746b..f8a48ef5de5 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -1,8 +1,20 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/grafana/authlib v0.0.0-20240611075137-331cbe4e840f h1:hvRCAv+TgcHu3i/Sd7lFJx84iEtgzDCYuk7OWeXatD0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/storage/unified/resource/grpc/authenticator.go b/pkg/storage/unified/resource/grpc/authenticator.go new file mode 100644 index 00000000000..f77cc87de92 --- /dev/null +++ b/pkg/storage/unified/resource/grpc/authenticator.go @@ -0,0 +1,163 @@ +package grpc + +import ( + "context" + "fmt" + "strconv" + + "github.com/grafana/authlib/authn" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +const ( + mdToken = "grafana-idtoken" + mdLogin = "grafana-login" + mdUserID = "grafana-user-id" + mdUserUID = "grafana-user-uid" + mdOrgName = "grafana-org-name" + mdOrgID = "grafana-org-id" + mdOrgRole = "grafana-org-role" +) + +// This is in a package we can no import +// var _ interceptors.Authenticator = (*Authenticator)(nil) + +type Authenticator struct { + IDTokenVerifier authn.Verifier[authn.IDTokenClaims] +} + +func (f *Authenticator) Authenticate(ctx context.Context) (context.Context, error) { + r, err := identity.GetRequester(ctx) + if err == nil && r != nil { + return ctx, nil // noop, requester exists + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, fmt.Errorf("no metadata found") + } + user, err := f.DecodeMetadata(ctx, md) + if err != nil { + return nil, err + } + return identity.WithRequester(ctx, user), nil +} + +func (f *Authenticator) DecodeMetadata(ctx context.Context, meta metadata.MD) (identity.Requester, error) { + // Avoid NPE/panic with getting keys + getter := func(key string) string { + v := meta.Get(key) + if len(v) > 0 { + return v[0] + } + return "" + } + + // First try the token + token := getter(mdToken) + if token != "" && f.IDTokenVerifier != nil { + claims, err := f.IDTokenVerifier.Verify(ctx, token) + if err != nil { + return nil, err + } + fmt.Printf("TODO, convert CLAIMS to an identity %+v\n", claims) + } + + user := &identity.StaticRequester{} + user.Login = getter(mdLogin) + if user.Login == "" { + return nil, fmt.Errorf("no login found in grpc metadata") + } + + // The namespaced verisons have a "-" in the key + // TODO, remove after this has been deployed to unified storage + if getter(mdUserID) == "" { + var err error + user.Namespace = identity.NamespaceUser + user.UserID, err = strconv.ParseInt(getter("grafana-userid"), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid user id: %w", err) + } + user.OrgID, err = strconv.ParseInt(getter("grafana-orgid"), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid org id: %w", err) + } + return user, nil + } + + ns, err := identity.ParseNamespaceID(getter(mdUserID)) + if err != nil { + return nil, fmt.Errorf("invalid user id: %w", err) + } + user.Namespace = ns.Namespace() + user.UserID, err = ns.ParseInt() + if err != nil { + return nil, fmt.Errorf("invalid user id: %w", err) + } + + ns, err = identity.ParseNamespaceID(getter(mdUserUID)) + if err != nil { + return nil, fmt.Errorf("invalid user id: %w", err) + } + user.UserUID = ns.ID() + + user.OrgName = getter(mdOrgName) + user.OrgID, err = strconv.ParseInt(getter(mdOrgID), 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid org id: %w", err) + } + user.OrgRole = identity.RoleType(getter(mdOrgRole)) + return user, nil +} + +func UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx, err := wrapContext(ctx) + if err != nil { + return err + } + return invoker(ctx, method, req, reply, cc, opts...) +} + +var _ grpc.UnaryClientInterceptor = UnaryClientInterceptor + +func StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + ctx, err := wrapContext(ctx) + if err != nil { + return nil, err + } + return streamer(ctx, desc, cc, method, opts...) +} + +var _ grpc.StreamClientInterceptor = StreamClientInterceptor + +func wrapContext(ctx context.Context) (context.Context, error) { + user, err := identity.GetRequester(ctx) + if err != nil { + return ctx, err + } + + // set grpc metadata into the context to pass to the grpc server + return metadata.NewOutgoingContext(ctx, encodeIdentityInMetadata(user)), nil +} + +func encodeIdentityInMetadata(user identity.Requester) metadata.MD { + return metadata.Pairs( + // This should be everything needed to recreate the user + mdToken, user.GetIDToken(), + + // Or we can create it directly + mdUserID, user.GetID().String(), + mdUserUID, user.GetUID().String(), + mdOrgName, user.GetOrgName(), + mdOrgID, strconv.FormatInt(user.GetOrgID(), 10), + mdOrgRole, string(user.GetOrgRole()), + mdLogin, user.GetLogin(), + + // TODO, Remove after this is deployed to unified storage + "grafana-userid", user.GetID().ID(), + "grafana-useruid", user.GetUID().ID(), + ) +} diff --git a/pkg/storage/unified/resource/grpc/authenticator_test.go b/pkg/storage/unified/resource/grpc/authenticator_test.go new file mode 100644 index 00000000000..d871cce721c --- /dev/null +++ b/pkg/storage/unified/resource/grpc/authenticator_test.go @@ -0,0 +1,34 @@ +package grpc + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apimachinery/identity" +) + +func TestBasicEncodeDecode(t *testing.T) { + before := &identity.StaticRequester{ + UserID: 123, + UserUID: "abc", + Login: "test", + Namespace: identity.NamespaceUser, + OrgID: 456, + OrgName: "org", + OrgRole: identity.RoleAdmin, + } + + auth := &Authenticator{} + + md := encodeIdentityInMetadata(before) + after, err := auth.DecodeMetadata(context.Background(), md) + require.NoError(t, err) + require.Equal(t, before.GetID(), after.GetID()) + require.Equal(t, before.GetUID(), after.GetUID()) + require.Equal(t, before.GetLogin(), after.GetLogin()) + require.Equal(t, before.GetOrgID(), after.GetOrgID()) + require.Equal(t, before.GetOrgName(), after.GetOrgName()) + require.Equal(t, before.GetOrgRole(), after.GetOrgRole()) +} From 30443190393a5a4cc07446bbb1cfd03b789a5f99 Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 20 Jun 2024 15:20:10 -0500 Subject: [PATCH 065/122] Explore: Allow Query Library drawer to delete queries (#89343) * WIP * WIP * Add URL additions to query library API * Add tag invalidation for refresh * add styles * Add verbiage from prototype * Translations generation --- .../QueryLibrary/QueryTemplatesList.tsx | 1 + .../QueryTemplatesTable/ActionsCell.tsx | 55 ++++++++++++++++++- .../QueryTemplatesTable/index.tsx | 2 +- .../QueryTemplatesTable/styles.tsx | 10 ++++ .../QueryLibrary/QueryTemplatesTable/types.ts | 1 + .../app/features/query-library/api/factory.ts | 9 ++- .../app/features/query-library/api/query.ts | 9 ++- public/app/features/query-library/index.ts | 3 +- public/app/features/query-library/types.ts | 4 ++ public/locales/en-US/grafana.json | 9 +++ public/locales/pseudo-LOCALE/grafana.json | 9 +++ 11 files changed, 105 insertions(+), 7 deletions(-) diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx index a1e8bb5c568..6989044a9c4 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx @@ -41,6 +41,7 @@ export function QueryTemplatesList() { const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || ''; return { index: index.toString(), + uid: queryTemplate.uid, datasourceRef, datasourceType, createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0, diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx index 6dd1c181d69..4ffd14edad6 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx @@ -1,16 +1,67 @@ import React from 'react'; +import { reportInteraction, getAppEvents } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; +import { IconButton } from '@grafana/ui'; +import { notifyApp } from 'app/core/actions'; +import { createSuccessNotification } from 'app/core/copy/appNotification'; +import { t } from 'app/core/internationalization'; +import { useDeleteQueryTemplateMutation } from 'app/features/query-library'; +import { dispatch } from 'app/store/store'; +import { ShowConfirmModalEvent } from 'app/types/events'; import ExploreRunQueryButton from '../../ExploreRunQueryButton'; +import { useQueryLibraryListStyles } from './styles'; + interface ActionsCellProps { + queryUid?: string; query?: DataQuery; rootDatasourceUid?: string; } -function ActionsCell({ query, rootDatasourceUid }: ActionsCellProps) { - return ; +function ActionsCell({ query, rootDatasourceUid, queryUid }: ActionsCellProps) { + const [deleteQueryTemplate] = useDeleteQueryTemplateMutation(); + const styles = useQueryLibraryListStyles(); + + const onDeleteQuery = (queryUid: string) => { + const performDelete = (queryUid: string) => { + deleteQueryTemplate({ uid: queryUid }); + dispatch(notifyApp(createSuccessNotification(t('explore.query-library.query-deleted', 'Query deleted')))); + reportInteraction('grafana_explore_query_library_deleted'); + }; + + getAppEvents().publish( + new ShowConfirmModalEvent({ + title: t('explore.query-library.delete-query-title', 'Delete query'), + text: t( + 'explore.query-library.delete-query-text', + "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?" + ), + yesText: t('query-library.delete-query-button', 'Delete query'), + icon: 'trash-alt', + onConfirm: () => performDelete(queryUid), + }) + ); + }; + + return ( +
+ { + if (queryUid) { + onDeleteQuery(queryUid); + } + }} + /> + +
+ ); } export default ActionsCell; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx index 56625e3cf15..0b90f8d29dd 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx @@ -26,7 +26,7 @@ const columns: Array> = [ id: 'actions', header: '', cell: ({ row: { original } }) => ( - + ), }, ]; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx index 1c7897e6e5d..715cd5d235b 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx @@ -34,4 +34,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ WebkitLineClamp: 1, overflow: 'hidden', }), + cell: css({ + display: 'flex', + alignItems: 'center', + '&:last-child': { + justifyContent: 'end', + }, + }), + actionButton: css({ + padding: theme.spacing(1), + }), }); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts index 62ddcf12420..59cc7b24b6a 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts @@ -7,4 +7,5 @@ export type QueryTemplateRow = { datasourceRef?: DataSourceRef | null; datasourceType?: string; createdAtTimestamp?: number; + uid?: string; }; diff --git a/public/app/features/query-library/api/factory.ts b/public/app/features/query-library/api/factory.ts index bff1973010f..09cc17f76f6 100644 --- a/public/app/features/query-library/api/factory.ts +++ b/public/app/features/query-library/api/factory.ts @@ -1,6 +1,6 @@ import { createApi } from '@reduxjs/toolkit/query/react'; -import { AddQueryTemplateCommand, QueryTemplate } from '../types'; +import { AddQueryTemplateCommand, DeleteQueryTemplateCommand, QueryTemplate } from '../types'; import { convertAddQueryTemplateCommandToDataQuerySpec, convertDataQueryResponseToQueryTemplates } from './mappers'; import { baseQuery } from './query'; @@ -21,6 +21,13 @@ export const queryLibraryApi = createApi({ }), invalidatesTags: ['QueryTemplatesList'], }), + deleteQueryTemplate: builder.mutation({ + query: ({ uid }) => ({ + url: `${uid}`, + method: 'DELETE', + }), + invalidatesTags: ['QueryTemplatesList'], + }), }), reducerPath: 'queryLibrary', }); diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts index 80464e1625b..b3baced6237 100644 --- a/public/app/features/query-library/api/query.ts +++ b/public/app/features/query-library/api/query.ts @@ -24,15 +24,20 @@ export enum QueryTemplateKinds { */ export const BASE_URL = `/apis/${API_VERSION}/namespaces/default/querytemplates/`; +// URL is optional for these requests +interface QueryLibraryBackendRequest extends Pick { + url?: string; +} + /** * TODO: similar code is duplicated in many places. To be unified in #86960 */ -export const baseQuery: BaseQueryFn, DataQuerySpecResponse, Error> = async ( +export const baseQuery: BaseQueryFn = async ( requestOptions ) => { try { const responseObservable = getBackendSrv().fetch({ - url: BASE_URL, + url: `${BASE_URL}${requestOptions.url ?? ''}`, showErrorAlert: true, method: requestOptions.method || 'GET', data: requestOptions.data, diff --git a/public/app/features/query-library/index.ts b/public/app/features/query-library/index.ts index 3c2e74e1f81..513b7ca5bb8 100644 --- a/public/app/features/query-library/index.ts +++ b/public/app/features/query-library/index.ts @@ -12,7 +12,8 @@ import { config } from '@grafana/runtime'; import { queryLibraryApi } from './api/factory'; import { mockData } from './api/mocks'; -export const { useAllQueryTemplatesQuery, useAddQueryTemplateMutation } = queryLibraryApi; +export const { useAllQueryTemplatesQuery, useAddQueryTemplateMutation, useDeleteQueryTemplateMutation } = + queryLibraryApi; export function isQueryLibraryEnabled() { return config.featureToggles.queryLibrary; diff --git a/public/app/features/query-library/types.ts b/public/app/features/query-library/types.ts index 030d292c901..9be092869d4 100644 --- a/public/app/features/query-library/types.ts +++ b/public/app/features/query-library/types.ts @@ -11,3 +11,7 @@ export type AddQueryTemplateCommand = { title: string; targets: DataQuery[]; }; + +export type DeleteQueryTemplateCommand = { + uid: string; +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2f81df6b661..66cd0278d59 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -532,6 +532,12 @@ "scan-for-older-logs": "Scan for older logs", "stop-scan": "Stop scan" }, + "query-library": { + "delete-query": "Delete query", + "delete-query-text": "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?", + "delete-query-title": "Delete query", + "query-deleted": "Query deleted" + }, "rich-history": { "close-tooltip": "Close query history", "datasource-a-z": "Data source A-Z", @@ -1559,6 +1565,9 @@ "role-label": "Role" } }, + "query-library": { + "delete-query-button": "Delete query" + }, "query-operation": { "header": { "collapse-row": "Collapse query row", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 288b9f25abc..ce0e02527a6 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -532,6 +532,12 @@ "scan-for-older-logs": "Ŝčäʼn ƒőř őľđęř ľőģş", "stop-scan": "Ŝŧőp şčäʼn" }, + "query-library": { + "delete-query": "Đęľęŧę qūęřy", + "delete-query-text": "Ÿőū'řę äþőūŧ ŧő řęmővę ŧĥįş qūęřy ƒřőm ŧĥę qūęřy ľįþřäřy. Ŧĥįş äčŧįőʼn čäʼnʼnőŧ þę ūʼnđőʼnę. Đő yőū ŵäʼnŧ ŧő čőʼnŧįʼnūę?", + "delete-query-title": "Đęľęŧę qūęřy", + "query-deleted": "Qūęřy đęľęŧęđ" + }, "rich-history": { "close-tooltip": "Cľőşę qūęřy ĥįşŧőřy", "datasource-a-z": "Đäŧä şőūřčę Å-Ż", @@ -1559,6 +1565,9 @@ "role-label": "Ŗőľę" } }, + "query-library": { + "delete-query-button": "Đęľęŧę qūęřy" + }, "query-operation": { "header": { "collapse-row": "Cőľľäpşę qūęřy řőŵ", From 3228b64fe60f23dbc455466f3ced2e7a5b06c280 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Thu, 20 Jun 2024 16:33:03 -0400 Subject: [PATCH 066/122] Alerting: Resend resolved notifications for ResolvedRetention duration (#88938) * Simple replace of State.Resolved with State.ResolvedAt * Retain ResolvedAt time between Normal->Normal transition * Introduce ResolvedRetention to keep sending recently resolved alerts * Make ResolvedRetention configurable with resolved_alert_retention * Tick-based LastSentAt for testing of ResendDelay and ResolvedRetention * Do not reset ResolvedAt during Normal->Pending transition Initially this was done to be inline with Prom ruler. However, Prom ruler doesn't keep track of Inactive->Pending/Alerting using the same alert instance, so it's more understandable that they choose not to retain ResolvedAt. In our case, since we use the same cached instance to represent the transition, it makes more sense to retain it. This should help alleviate some odd situations where temporarily entering Pending will stop future resolved notifications that would have happened because of ResolvedRetention. * Pointers for ResolvedAt & LastSentAt To avoid awkward time.Time{}.Unix() defaults on persist --- conf/defaults.ini | 3 + conf/sample.ini | 7 +- pkg/services/ngalert/ngalert.go | 1 + pkg/services/ngalert/schedule/alert_rule.go | 9 +- .../ngalert/schedule/alert_rule_test.go | 98 +++++++++++++- pkg/services/ngalert/schedule/testing.go | 6 +- pkg/services/ngalert/state/compat.go | 12 +- pkg/services/ngalert/state/compat_test.go | 9 +- pkg/services/ngalert/state/manager.go | 29 ++++- .../ngalert/state/manager_private_test.go | 32 ++--- pkg/services/ngalert/state/manager_test.go | 123 +++++++++++++++++- pkg/services/ngalert/state/state.go | 58 +++++---- pkg/services/ngalert/state/state_test.go | 83 ++++++++---- pkg/setting/setting_unified_alerting.go | 11 +- 14 files changed, 377 insertions(+), 104 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 898b0261fa2..224555260f2 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1304,6 +1304,9 @@ disable_jitter = false # Retention period for Alertmanager notification log entries. notification_log_retention = 5d +# Duration for which a resolved alert state transition will continue to be sent to the Alertmanager. +resolved_alert_retention = 15m + [unified_alerting.screenshots] # Enable screenshots in notifications. You must have either installed the Grafana image rendering # plugin, or set up Grafana to use a remote rendering service. diff --git a/conf/sample.ini b/conf/sample.ini index b1d61c44187..edd415de28b 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -792,7 +792,7 @@ ;role_attribute_strict = false ;groups_attribute_path = ;id_token_attribute_name = -;team_ids_attribute_path +;team_ids_attribute_path ;auth_url = https://foo.bar/login/oauth/authorize ;token_url = https://foo.bar/login/oauth/access_token ;api_url = https://foo.bar/user @@ -1290,6 +1290,9 @@ # Retention period for Alertmanager notification log entries. ;notification_log_retention = 5d +# Duration for which a resolved alert state transition will continue to be sent to the Alertmanager. +;resolved_alert_retention = 15m + [unified_alerting.screenshots] # Enable screenshots in notifications. You must have either installed the Grafana image rendering # plugin, or set up Grafana to use a remote rendering service. @@ -1837,4 +1840,4 @@ timeout = 30s #################################### Public Dashboards ##################################### [public_dashboards] # Set to false to disable public dashboards -;enabled = true \ No newline at end of file +;enabled = true diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index ec86f1ed5f0..952e1d86814 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -377,6 +377,7 @@ func (ng *AlertNG) init() error { RulesPerRuleGroupLimit: ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit, Tracer: ng.tracer, Log: log.New("ngalert.state.manager"), + ResolvedRetention: ng.Cfg.UnifiedAlerting.ResolvedAlertRetention, } logger := log.New("ngalert.state.manager.persist") statePersister := state.NewSyncStatePersisiter(logger, cfg) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 6c3d58ec3e8..f033448f10f 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -8,6 +8,10 @@ import ( "time" "github.com/benbjohnson/clock" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/datasources" @@ -19,9 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" ) // Rule represents a single piece of work that is executed periodically by the ruler. @@ -418,7 +419,7 @@ func (a *alertRule) evaluate(ctx context.Context, key ngmodels.AlertRuleKey, f f processDuration.Observe(a.clock.Now().Sub(start).Seconds()) start = a.clock.Now() - alerts := state.FromStateTransitionToPostableAlerts(processedStates, a.stateManager, a.appURL) + alerts := state.FromStateTransitionToPostableAlerts(e.scheduledAt, processedStates, a.stateManager, a.appURL) span.AddEvent("results processed", trace.WithAttributes( attribute.Int64("state_transitions", int64(len(processedStates))), attribute.Int64("alerts_to_send", int64(len(alerts.PostableAlerts))), diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 12eb90ec661..875c098302e 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -2,7 +2,7 @@ package schedule import ( "bytes" - context "context" + "context" "fmt" "math" "math/rand" @@ -11,19 +11,21 @@ import ( "testing" "time" - alertingModels "github.com/grafana/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" prometheusModel "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" - mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + alertingModels "github.com/grafana/alerting/models" + "github.com/grafana/grafana/pkg/infra/log" - definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" - models "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/util" ) @@ -762,8 +764,94 @@ func TestRuleRoutine(t *testing.T) { require.NotEmpty(t, sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID)) }) + + t.Run("when there are resolved alerts they should keep sending until retention period is over", func(t *testing.T) { + rule := gen.With(withQueryForState(t, eval.Normal), models.RuleMuts.WithInterval(time.Second)).GenerateRef() + + evalAppliedChan := make(chan time.Time) + + sender := NewSyncAlertsSenderMock() + sender.EXPECT().Send(mock.Anything, rule.GetKey(), mock.Anything).Return() + + sch, ruleStore, _, _ := createSchedule(evalAppliedChan, sender) + sch.stateManager.ResolvedRetention = 4 * time.Second + sch.stateManager.ResendDelay = 2 * time.Second + sch.stateManager.Put([]*state.State{ + stateForRule(rule, sch.clock.Now(), eval.Alerting), // Add existing Alerting state so evals will resolve. + }) + + ruleStore.PutRule(context.Background(), rule) + factory := ruleFactoryFromScheduler(sch) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ruleInfo := factory.new(ctx, rule) + + go func() { + _ = ruleInfo.Run(rule.GetKey()) + }() + + // Evaluate 10 times: + // 1. Send resolve #1. + // 2. 2s resend delay. + // 3. Send resolve #2. + // 4. 2s resend delay. + // 5. Send resolve #3. + // 6. No more sends, 4s retention period is over. + expectedResolves := map[time.Time]struct{}{ + sch.clock.Now().Add(1 * time.Second): {}, + sch.clock.Now().Add(3 * time.Second): {}, + sch.clock.Now().Add(5 * time.Second): {}, + } + calls := 0 + for i := 1; i < 10; i++ { + ts := sch.clock.Now().Add(time.Duration(int64(i)*rule.IntervalSeconds) * time.Second) + ruleInfo.Eval(&Evaluation{ + scheduledAt: ts, + rule: rule, + }) + waitForTimeChannel(t, evalAppliedChan) + + if _, ok := expectedResolves[ts]; ok { + calls++ + prevCallAlerts, ok := sender.Calls()[calls-1].Arguments[2].(definitions.PostableAlerts) + assert.Truef(t, ok, fmt.Sprintf("expected argument of function was supposed to be 'definitions.PostableAlerts' but got %T", sender.Calls()[calls-1].Arguments[2])) + assert.Len(t, prevCallAlerts.PostableAlerts, 1) + } + sender.AssertNumberOfCalls(t, "Send", calls) + } + }) } func ruleFactoryFromScheduler(sch *schedule) ruleFactory { return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, &sch.schedulableAlertRules, sch.clock, sch.featureToggles, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc) } + +func stateForRule(rule *models.AlertRule, ts time.Time, evalState eval.State) *state.State { + s := &state.State{ + OrgID: rule.OrgID, + AlertRuleUID: rule.UID, + CacheID: 0, + State: evalState, + Annotations: make(map[string]string), + Labels: make(map[string]string), + StartsAt: ts, + EndsAt: ts, + ResolvedAt: &ts, + LastSentAt: &ts, + LastEvaluationTime: ts, + } + for k, v := range rule.Labels { + s.Labels[k] = v + } + for k, v := range state.GetRuleExtraLabels(&logtest.Fake{}, rule, "", true) { + if _, ok := s.Labels[k]; !ok { + s.Labels[k] = v + } + } + il := models.InstanceLabels(s.Labels) + s.Labels = data.Labels(il) + id := il.Fingerprint() + s.CacheID = id + + return s +} diff --git a/pkg/services/ngalert/schedule/testing.go b/pkg/services/ngalert/schedule/testing.go index 2f00c213ac3..d182329cf16 100644 --- a/pkg/services/ngalert/schedule/testing.go +++ b/pkg/services/ngalert/schedule/testing.go @@ -7,15 +7,17 @@ import ( "testing" "time" - definitions "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" - mock "github.com/stretchr/testify/mock" ) // waitForTimeChannel blocks the execution until either the channel ch has some data or a timeout of 10 second expires. // Timeout will cause the test to fail. // Returns the data from the channel. func waitForTimeChannel(t *testing.T, ch chan time.Time) time.Time { + t.Helper() select { case result := <-ch: return result diff --git a/pkg/services/ngalert/state/compat.go b/pkg/services/ngalert/state/compat.go index 673e9e56810..1a070cfd5ac 100644 --- a/pkg/services/ngalert/state/compat.go +++ b/pkg/services/ngalert/state/compat.go @@ -10,11 +10,12 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" - alertingModels "github.com/grafana/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/common/model" + alertingModels "github.com/grafana/alerting/models" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -73,7 +74,7 @@ func StateToPostableAlert(transition StateTransition, appURL *url.URL) *models.P } state := alertState.State - if alertState.Resolved { + if alertState.ResolvedAt != nil { // If this is a resolved alert, we need to send an alert with the correct labels such that they will expire the previous alert. // In most cases the labels on the state will be correct, however when the previous alert was a NoData or Error alert, we need to // ensure to modify it appropriately. @@ -139,13 +140,12 @@ func errorAlert(labels, annotations data.Labels, alertState *State, urlStr strin } } -func FromStateTransitionToPostableAlerts(firingStates []StateTransition, stateManager *Manager, appURL *url.URL) apimodels.PostableAlerts { +func FromStateTransitionToPostableAlerts(evaluatedAt time.Time, firingStates []StateTransition, stateManager *Manager, appURL *url.URL) apimodels.PostableAlerts { alerts := apimodels.PostableAlerts{PostableAlerts: make([]models.PostableAlert, 0, len(firingStates))} - ts := time.Now() sentAlerts := make([]*State, 0, len(firingStates)) for _, alertState := range firingStates { - if !alertState.NeedsSending(stateManager.ResendDelay) { + if !alertState.NeedsSending(stateManager.ResendDelay, stateManager.ResolvedRetention) { continue } alert := StateToPostableAlert(alertState, appURL) @@ -153,7 +153,7 @@ func FromStateTransitionToPostableAlerts(firingStates []StateTransition, stateMa if alertState.StateReason == ngModels.StateReasonMissingSeries { // do not put stale state back to state manager continue } - alertState.LastSentAt = ts + alertState.LastSentAt = &evaluatedAt sentAlerts = append(sentAlerts, alertState.State) } stateManager.Put(sentAlerts) diff --git a/pkg/services/ngalert/state/compat_test.go b/pkg/services/ngalert/state/compat_test.go index e0f889a96ea..bd0189badb8 100644 --- a/pkg/services/ngalert/state/compat_test.go +++ b/pkg/services/ngalert/state/compat_test.go @@ -9,12 +9,13 @@ import ( "github.com/benbjohnson/clock" "github.com/go-openapi/strfmt" - alertingModels "github.com/grafana/alerting/models" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + alertingModels "github.com/grafana/alerting/models" + "github.com/grafana/grafana/pkg/services/ngalert/eval" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/util" @@ -267,7 +268,9 @@ func TestStateToPostableAlertFromNodataError(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { alertState := randomTransition(tc.from, tc.to) - alertState.Resolved = tc.resolved + if tc.resolved { + alertState.ResolvedAt = &alertState.LastEvaluationTime + } alertState.Labels = data.Labels(standardLabels) result := StateToPostableAlert(alertState, appURL) require.Equal(t, tc.expectedLabels, result.Labels) @@ -339,7 +342,7 @@ func randomTransition(from, to eval.State) StateTransition { EndsAt: randomTimeInFuture(), LastEvaluationTime: randomTimeInPast(), EvaluationDuration: randomDuration(), - LastSentAt: randomTimeInPast(), + LastSentAt: util.Pointer(randomTimeInPast()), Annotations: make(map[string]string), Labels: make(map[string]string), Values: make(map[string]float64), diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 10050b60037..b4a64d4cb32 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -39,9 +39,10 @@ type Manager struct { metrics *metrics.State tracer tracing.Tracer - clock clock.Clock - cache *cache - ResendDelay time.Duration + clock clock.Clock + cache *cache + ResendDelay time.Duration + ResolvedRetention time.Duration instanceStore InstanceStore images ImageCapturer @@ -73,6 +74,9 @@ type ManagerCfg struct { DisableExecution bool + // Duration for which a resolved alert state transition will continue to be sent to the Alertmanager. + ResolvedRetention time.Duration + Tracer tracing.Tracer Log log.Logger } @@ -88,6 +92,7 @@ func NewManager(cfg ManagerCfg, statePersister StatePersister) *Manager { m := &Manager{ cache: c, ResendDelay: ResendDelay, // TODO: make this configurable + ResolvedRetention: cfg.ResolvedRetention, log: cfg.Log, metrics: cfg.Metrics, instanceStore: cfg.InstanceStore, @@ -245,7 +250,11 @@ func (st *Manager) DeleteStateByRuleUID(ctx context.Context, ruleKey ngModels.Al s.SetNormal(reason, startsAt, now) // Set Resolved property so the scheduler knows to send a postable alert // to Alertmanager. - s.Resolved = oldState == eval.Alerting || oldState == eval.Error || oldState == eval.NoData + if oldState == eval.Alerting || oldState == eval.Error || oldState == eval.NoData { + s.ResolvedAt = &now + } else { + s.ResolvedAt = nil + } s.LastEvaluationTime = now s.Values = map[string]float64{} transitions = append(transitions, StateTransition{ @@ -418,9 +427,15 @@ func (st *Manager) setNextState(ctx context.Context, alertRule *ngModels.AlertRu // Set Resolved property so the scheduler knows to send a postable alert // to Alertmanager. - currentState.Resolved = oldState == eval.Alerting && currentState.State == eval.Normal + newlyResolved := false + if oldState == eval.Alerting && currentState.State == eval.Normal { + currentState.ResolvedAt = &result.EvaluatedAt + newlyResolved = true + } else if currentState.State != eval.Normal && currentState.State != eval.Pending { // Retain the last resolved time for Normal->Normal and Normal->Pending. + currentState.ResolvedAt = nil + } - if shouldTakeImage(currentState.State, oldState, currentState.Image, currentState.Resolved) { + if shouldTakeImage(currentState.State, oldState, currentState.Image, newlyResolved) { image, err := takeImage(ctx, st.images, alertRule) if err != nil { logger.Warn("Failed to take an image", @@ -505,7 +520,7 @@ func (st *Manager) deleteStaleStatesFromCache(ctx context.Context, logger log.Lo s.LastEvaluationTime = evaluatedAt if oldState == eval.Alerting { - s.Resolved = true + s.ResolvedAt = &evaluatedAt image, err := takeImage(ctx, st.images, alertRule) if err != nil { logger.Warn("Failed to take an image", diff --git a/pkg/services/ngalert/state/manager_private_test.go b/pkg/services/ngalert/state/manager_private_test.go index 8740186deac..6f2993d6d88 100644 --- a/pkg/services/ngalert/state/manager_private_test.go +++ b/pkg/services/ngalert/state/manager_private_test.go @@ -563,7 +563,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t2, EndsAt: t2, LastEvaluationTime: t2, - Resolved: true, + ResolvedAt: &t2, }, }, }, @@ -622,7 +622,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -1051,7 +1051,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -1091,7 +1091,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -1133,7 +1133,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -1175,7 +1175,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -1275,7 +1275,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t2, EndsAt: t2, LastEvaluationTime: t2, - Resolved: true, + ResolvedAt: &t2, }, }, }, @@ -1304,6 +1304,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t2, EndsAt: t2, LastEvaluationTime: t3, + ResolvedAt: &t2, }, }, }, @@ -1935,7 +1936,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, }, @@ -2163,7 +2164,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -2191,7 +2192,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -2221,7 +2222,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -2251,7 +2252,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, { @@ -2314,7 +2315,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t2, EndsAt: t2, LastEvaluationTime: t2, - Resolved: true, + ResolvedAt: &t2, }, }, }, @@ -2330,6 +2331,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t2, EndsAt: t2, LastEvaluationTime: t3, + ResolvedAt: &t2, }, }, }, @@ -3060,7 +3062,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t1, EndsAt: t3, LastEvaluationTime: t3, - Resolved: true, + ResolvedAt: &t3, }, }, }, @@ -3480,7 +3482,7 @@ func TestProcessEvalResults_StateTransitions(t *testing.T) { StartsAt: t2, EndsAt: t2, LastEvaluationTime: t2, - Resolved: true, + ResolvedAt: &t2, }, }, }, diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index edb836b4dac..b31eab7402d 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "math/rand" + "net/url" "sort" "strings" "testing" @@ -294,7 +295,7 @@ func TestProcessEvalResults(t *testing.T) { evaluationDuration := 10 * time.Millisecond evaluationInterval := 10 * time.Second - t1 := time.Time{}.Add(evaluationInterval) + t1 := time.Unix(0, 0).Add(evaluationInterval) tn := func(n int) time.Time { return t1.Add(time.Duration(n-1) * evaluationInterval) @@ -424,6 +425,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t1, EndsAt: t1.Add(state.ResendDelay * 4), LastEvaluationTime: t1, + LastSentAt: &t1, }, }, }, @@ -471,6 +473,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t2, EndsAt: t2.Add(state.ResendDelay * 4), LastEvaluationTime: t2, + LastSentAt: &t2, }, }, }, @@ -501,6 +504,94 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(4), EndsAt: tn(4).Add(state.ResendDelay * 4), LastEvaluationTime: tn(4), + LastSentAt: util.Pointer(tn(4)), + }, + }, + }, + { + desc: "alerting -> normal resolves and sets ResolvedAt", + alertRule: baseRule, + evalResults: map[time.Time]eval.Results{ + t1: { + newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), + }, + t2: { + newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), + }, + }, + expectedAnnotations: 2, + expectedStates: []*state.State{ + { + Labels: labels["system + rule + labels1"], + ResultFingerprint: labels1.Fingerprint(), + State: eval.Normal, + LatestResult: newEvaluation(t2, eval.Normal), + StartsAt: t2, + EndsAt: t2, + LastEvaluationTime: t2, + ResolvedAt: &t2, + LastSentAt: &t2, + }, + }, + }, + { + desc: "alerting -> normal -> normal resolves and maintains ResolvedAt", + alertRule: baseRule, + evalResults: map[time.Time]eval.Results{ + t1: { + newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), + }, + t2: { + newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), + }, + t3: { + newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), + }, + }, + expectedAnnotations: 2, + expectedStates: []*state.State{ + { + Labels: labels["system + rule + labels1"], + ResultFingerprint: labels1.Fingerprint(), + State: eval.Normal, + LatestResult: newEvaluation(t3, eval.Normal), + StartsAt: t2, + EndsAt: t2, + LastEvaluationTime: t3, + ResolvedAt: &t2, + LastSentAt: &t2, + }, + }, + }, + { + desc: "pending -> alerting -> normal -> pending resolves and resets ResolvedAt at t4", + alertRule: baseRuleWith(m.WithForNTimes(1)), + evalResults: map[time.Time]eval.Results{ + t1: { + newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), + }, + t2: { + newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), // Alerting. + }, + t3: { + newResult(eval.WithState(eval.Normal), eval.WithLabels(labels1)), + }, + tn(4): { + newResult(eval.WithState(eval.Alerting), eval.WithLabels(labels1)), // Pending. + }, + }, + expectedAnnotations: 4, + expectedStates: []*state.State{ + { + Labels: labels["system + rule + labels1"], + ResultFingerprint: labels1.Fingerprint(), + State: eval.Pending, + LatestResult: newEvaluation(tn(4), eval.Alerting), + StartsAt: tn(4), + EndsAt: tn(4).Add(state.ResendDelay * 4), + LastEvaluationTime: tn(4), + ResolvedAt: &t3, + LastSentAt: &t3, }, }, }, @@ -534,6 +625,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(4), EndsAt: tn(4).Add(state.ResendDelay * 4), LastEvaluationTime: tn(5), + LastSentAt: util.Pointer(tn(3)), // 30s resend delay causing the last sent at to be t3. }, }, }, @@ -564,6 +656,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(4), EndsAt: tn(4).Add(state.ResendDelay * 4), LastEvaluationTime: tn(4), + LastSentAt: &t3, // Resend delay is 30s, so last sent at is t3. }, }, }, @@ -672,6 +765,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(5), EndsAt: tn(5).Add(state.ResendDelay * 4), LastEvaluationTime: tn(5), + LastSentAt: util.Pointer(tn(5)), }, }, }, @@ -696,6 +790,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t2, EndsAt: t2.Add(state.ResendDelay * 4), LastEvaluationTime: t2, + LastSentAt: &t2, }, }, }, @@ -729,6 +824,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t2, EndsAt: t2.Add(state.ResendDelay * 4), LastEvaluationTime: t2, + LastSentAt: &t2, }, }, }, @@ -772,6 +868,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t2, EndsAt: t2.Add(state.ResendDelay * 4), LastEvaluationTime: t2, + LastSentAt: &t2, }, }, }, @@ -808,6 +905,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t2, EndsAt: t2.Add(state.ResendDelay * 4), LastEvaluationTime: t2, + LastSentAt: &t2, }, }, }, @@ -839,6 +937,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t3, EndsAt: tn(4).Add(state.ResendDelay * 4), LastEvaluationTime: tn(4), + LastSentAt: &t3, // Resend delay is 30s, so last sent at is t3. }, }, }, @@ -870,6 +969,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(4), EndsAt: tn(4).Add(state.ResendDelay * 4), LastEvaluationTime: tn(4), + LastSentAt: util.Pointer(tn(4)), }, }, }, @@ -956,6 +1056,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(5), EndsAt: tn(5).Add(state.ResendDelay * 4), LastEvaluationTime: tn(5), + LastSentAt: util.Pointer(tn(5)), }, }, }, @@ -988,6 +1089,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t2, EndsAt: t2.Add(state.ResendDelay * 4), LastEvaluationTime: t2, + LastSentAt: &t2, EvaluationDuration: evaluationDuration, Annotations: map[string]string{"annotation": "test", "Error": "[sse.dataQueryError] failed to execute query [A]: this is an error"}, }, @@ -1021,6 +1123,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t3, EndsAt: tn(4).Add(state.ResendDelay * 4), LastEvaluationTime: tn(4), + LastSentAt: &t3, // Resend delay is 30s, so last sent at is t3. }, }, }, @@ -1052,6 +1155,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(4), EndsAt: tn(4).Add(state.ResendDelay * 4), LastEvaluationTime: tn(4), + LastSentAt: util.Pointer(tn(4)), }, }, }, @@ -1139,6 +1243,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(4), EndsAt: tn(6).Add(state.ResendDelay * 4), LastEvaluationTime: tn(6), + LastSentAt: util.Pointer(tn(6)), // After 30s resend delay, last sent at is t6. }, }, }, @@ -1169,6 +1274,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(8), EndsAt: tn(8).Add(state.ResendDelay * 4), LastEvaluationTime: tn(8), + LastSentAt: util.Pointer(tn(5)), }, }, }, @@ -1199,6 +1305,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: tn(6), EndsAt: tn(6).Add(state.ResendDelay * 4), LastEvaluationTime: tn(6), + LastSentAt: util.Pointer(tn(5)), }, }, }, @@ -1265,6 +1372,7 @@ func TestProcessEvalResults(t *testing.T) { StartsAt: t3, EndsAt: t3.Add(state.ResendDelay * 4), LastEvaluationTime: t3, + LastSentAt: &t1, // Resend delay is 30s, so last sent at is t1. }, }, }, @@ -1306,8 +1414,9 @@ func TestProcessEvalResults(t *testing.T) { res[i].EvaluatedAt = evalTime } clk.Set(evalTime) - _ = st.ProcessEvalResults(context.Background(), evalTime, tc.alertRule, res, systemLabels) + processedStates := st.ProcessEvalResults(context.Background(), evalTime, tc.alertRule, res, systemLabels) results += len(res) + _ = state.FromStateTransitionToPostableAlerts(evalTime, processedStates, st, &url.URL{}) // Set LastSentAt. } states := st.GetStatesForRuleUID(tc.alertRule.OrgID, tc.alertRule.UID) @@ -1670,7 +1779,7 @@ func TestStaleResults(t *testing.T) { assert.Equal(t, models.StateReasonMissingSeries, s.StateReason) assert.Equal(t, clk.Now(), s.EndsAt) if s.CacheID == state2 { - assert.Truef(t, s.Resolved, "Returned stale state should have Resolved set to true") + assert.Equalf(t, clk.Now(), *s.ResolvedAt, "Returned stale state should have ResolvedAt set") } key, err := s.GetAlertInstanceKey() require.NoError(t, err) @@ -1819,11 +1928,11 @@ func TestDeleteStateByRuleUID(t *testing.T) { assert.Equal(t, expectedReason, s.StateReason) if oldState.State == eval.Normal { assert.Equal(t, oldState.StartsAt, s.StartsAt) - assert.False(t, s.Resolved) + assert.Zero(t, s.ResolvedAt) } else { assert.Equal(t, clk.Now(), s.StartsAt) if oldState.State == eval.Alerting { - assert.True(t, s.Resolved) + assert.Equal(t, clk.Now(), *s.ResolvedAt) } } assert.Equal(t, clk.Now(), s.EndsAt) @@ -1959,11 +2068,11 @@ func TestResetStateByRuleUID(t *testing.T) { assert.Equal(t, models.StateReasonPaused, s.StateReason) if oldState.State == eval.Normal { assert.Equal(t, oldState.StartsAt, s.StartsAt) - assert.False(t, s.Resolved) + assert.Zero(t, s.ResolvedAt) } else { assert.Equal(t, clk.Now(), s.StartsAt) if oldState.State == eval.Alerting { - assert.True(t, s.Resolved) + assert.Equal(t, clk.Now(), *s.ResolvedAt) } } assert.Equal(t, clk.Now(), s.EndsAt) diff --git a/pkg/services/ngalert/state/state.go b/pkg/services/ngalert/state/state.go index 206747c652e..260dd92aeef 100644 --- a/pkg/services/ngalert/state/state.go +++ b/pkg/services/ngalert/state/state.go @@ -45,10 +45,6 @@ type State struct { // can still contain the results of previous evaluations. Error error - // Resolved is set to true if this state is the transitional state between Firing and Normal. - // All subsequent states will be false until the next transition from Firing to Normal. - Resolved bool - // Image contains an optional image for the state. It tends to be included in notifications // as a visualization to show why the alert fired. Image *models.Image @@ -65,9 +61,15 @@ type State struct { // conditions. Values map[string]float64 - StartsAt time.Time - EndsAt time.Time - LastSentAt time.Time + StartsAt time.Time + // EndsAt is different from the Prometheus EndsAt as EndsAt is updated for both Normal states + // and states that have been resolved. It cannot be used to determine when a state was resolved. + EndsAt time.Time + // ResolvedAt is set when the state is first resolved. That is to say, when the state first transitions + // from Alerting, NoData, or Error to Normal. It is reset to zero when the state transitions from Normal + // to any other state. + ResolvedAt *time.Time + LastSentAt *time.Time LastEvaluationString string LastEvaluationTime time.Time EvaluationDuration time.Duration @@ -134,14 +136,6 @@ func (a *State) SetNormal(reason string, startsAt, endsAt time.Time) { a.Error = nil } -// Resolve sets the State to Normal. It updates the StateReason, the end time, and sets Resolved to true. -func (a *State) Resolve(reason string, endsAt time.Time) { - a.State = eval.Normal - a.StateReason = reason - a.Resolved = true - a.EndsAt = endsAt -} - // Maintain updates the end time using the most recent evaluation. func (a *State) Maintain(interval int64, evaluatedAt time.Time) { a.EndsAt = nextEndsTime(interval, evaluatedAt) @@ -400,19 +394,31 @@ func resultKeepLast(state *State, rule *models.AlertRule, result eval.Result, lo } } -func (a *State) NeedsSending(resendDelay time.Duration) bool { - switch a.State { - case eval.Pending: - // We do not send notifications for pending states +// NeedsSending returns true if the given state needs to be sent to the Alertmanager. +// Reasons for sending include: +// - The state has been resolved since the last notification. +// - The state is firing and the last notification was sent at least resendDelay ago. +// - The state was resolved within the resolvedRetention period, and the last notification was sent at least resendDelay ago. +func (a *State) NeedsSending(resendDelay time.Duration, resolvedRetention time.Duration) bool { + if a.State == eval.Pending { + // We do not send notifications for pending states. return false - case eval.Normal: - // We should send a notification if the state is Normal because it was resolved - return a.Resolved - default: - // We should send, and re-send notifications, each time LastSentAt is <= LastEvaluationTime + resendDelay - nextSent := a.LastSentAt.Add(resendDelay) - return nextSent.Before(a.LastEvaluationTime) || nextSent.Equal(a.LastEvaluationTime) } + + // We should send a notification if the state has been resolved since the last notification. + if a.ResolvedAt != nil && (a.LastSentAt == nil || a.ResolvedAt.After(*a.LastSentAt)) { + return true + } + + // For normal states, we should only be sending if this is a resolved notification or a re-send of the resolved + // notification within the resolvedRetention period. + if a.State == eval.Normal && (a.ResolvedAt == nil || a.LastEvaluationTime.Sub(*a.ResolvedAt) > resolvedRetention) { + return false + } + + // We should send, and re-send notifications, each time LastSentAt is <= LastEvaluationTime + resendDelay. + // This can include normal->normal transitions that were resolved in recent past evaluations. + return a.LastSentAt == nil || !a.LastSentAt.Add(resendDelay).After(a.LastEvaluationTime) } func (a *State) Equals(b *State) bool { diff --git a/pkg/services/ngalert/state/state_test.go b/pkg/services/ngalert/state/state_test.go index bc00efe875a..f079d90e391 100644 --- a/pkg/services/ngalert/state/state_test.go +++ b/pkg/services/ngalert/state/state_test.go @@ -11,11 +11,12 @@ import ( "github.com/benbjohnson/clock" "github.com/golang/mock/gomock" "github.com/google/uuid" - "github.com/grafana/alerting/models" "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/alerting/models" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -350,10 +351,11 @@ func TestEnd(t *testing.T) { func TestNeedsSending(t *testing.T) { evaluationTime, _ := time.Parse("2006-01-02", "2021-03-25") testCases := []struct { - name string - resendDelay time.Duration - expected bool - testState *State + name string + resendDelay time.Duration + resolvedRetention time.Duration + expected bool + testState *State }{ { name: "state: alerting and LastSentAt before LastEvaluationTime + ResendDelay", @@ -362,7 +364,7 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.Alerting, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime.Add(-2 * time.Minute), + LastSentAt: util.Pointer(evaluationTime.Add(-2 * time.Minute)), }, }, { @@ -372,7 +374,7 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.Alerting, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime, + LastSentAt: util.Pointer(evaluationTime), }, }, { @@ -382,7 +384,7 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.Alerting, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime.Add(-1 * time.Minute), + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), }, }, { @@ -400,18 +402,54 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.Alerting, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime, + LastSentAt: util.Pointer(evaluationTime), }, }, { - name: "state: normal + resolved should send without waiting", + name: "state: normal + resolved should send without waiting if ResolvedAt > LastSentAt", resendDelay: 1 * time.Minute, expected: true, testState: &State{ State: eval.Normal, - Resolved: true, + ResolvedAt: util.Pointer(evaluationTime), LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime, + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), + }, + }, + { + name: "state: normal + recently resolved should send with wait", + resendDelay: 1 * time.Minute, + resolvedRetention: 15 * time.Minute, + expected: true, + testState: &State{ + State: eval.Normal, + ResolvedAt: util.Pointer(evaluationTime.Add(-2 * time.Minute)), + LastEvaluationTime: evaluationTime, + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), + }, + }, + { + name: "state: normal + recently resolved should not send without wait", + resendDelay: 2 * time.Minute, + resolvedRetention: 15 * time.Minute, + expected: false, + testState: &State{ + State: eval.Normal, + ResolvedAt: util.Pointer(evaluationTime.Add(-2 * time.Minute)), + LastEvaluationTime: evaluationTime, + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), + }, + }, + { + name: "state: normal + not recently resolved should not send even with wait", + resendDelay: 1 * time.Minute, + resolvedRetention: 15 * time.Minute, + expected: false, + testState: &State{ + State: eval.Normal, + ResolvedAt: util.Pointer(evaluationTime.Add(-16 * time.Minute)), + LastEvaluationTime: evaluationTime, + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), }, }, { @@ -420,9 +458,9 @@ func TestNeedsSending(t *testing.T) { expected: false, testState: &State{ State: eval.Normal, - Resolved: false, + ResolvedAt: util.Pointer(time.Time{}), LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime.Add(-1 * time.Minute), + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), }, }, { @@ -432,7 +470,7 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.NoData, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime.Add(-1 * time.Minute), + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), }, }, { @@ -442,7 +480,7 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.NoData, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second), + LastSentAt: util.Pointer(evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second)), }, }, { @@ -452,7 +490,7 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.Error, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime.Add(-1 * time.Minute), + LastSentAt: util.Pointer(evaluationTime.Add(-1 * time.Minute)), }, }, { @@ -462,14 +500,14 @@ func TestNeedsSending(t *testing.T) { testState: &State{ State: eval.Error, LastEvaluationTime: evaluationTime, - LastSentAt: evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second), + LastSentAt: util.Pointer(evaluationTime.Add(-time.Duration(rand.Int63n(59)+1) * time.Second)), }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expected, tc.testState.NeedsSending(tc.resendDelay)) + assert.Equal(t, tc.expected, tc.testState.NeedsSending(tc.resendDelay, tc.resolvedRetention)) }) } } @@ -531,13 +569,6 @@ func TestGetLastEvaluationValuesForCondition(t *testing.T) { }) } -func TestResolve(t *testing.T) { - s := State{State: eval.Alerting, EndsAt: time.Now().Add(time.Minute)} - expected := State{State: eval.Normal, StateReason: "This is a reason", EndsAt: time.Now(), Resolved: true} - s.Resolve("This is a reason", expected.EndsAt) - assert.Equal(t, expected, s) -} - func TestShouldTakeImage(t *testing.T) { tests := []struct { name string diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index 4cd4e9dde85..1d205dc6faa 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -6,11 +6,12 @@ import ( "strings" "time" - alertingCluster "github.com/grafana/alerting/cluster" dstls "github.com/grafana/dskit/crypto/tls" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "gopkg.in/ini.v1" + alertingCluster "github.com/grafana/alerting/cluster" + "github.com/grafana/grafana/pkg/util" ) @@ -113,6 +114,9 @@ type UnifiedAlertingSettings struct { // Retention period for Alertmanager notification log entries. NotificationLogRetention time.Duration + + // Duration for which a resolved alert state transition will continue to be sent to the Alertmanager. + ResolvedAlertRetention time.Duration } type RecordingRuleSettings struct { @@ -435,6 +439,11 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { return err } + uaCfg.ResolvedAlertRetention, err = gtime.ParseDuration(valueAsString(ua, "resolved_alert_retention", (15 * time.Minute).String())) + if err != nil { + return err + } + cfg.UnifiedAlerting = uaCfg return nil } From b07592620279f16b0353444e7aba3c457c50d7ec Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 20 Jun 2024 16:52:03 -0400 Subject: [PATCH 067/122] Alerting: Time Intervals API (#88201) * expose ngalert API to public * add delete action to time-intervals * introduce time-interval model generated by app-platform-sdk from CUE model the fields of the model are chosen to be compatible with the current model * implement api server * add feature flag alertingApiServer ---- Test Infra * update helper to support creating custom users with enterprise permissions * add generator for Interval model --- .github/CODEOWNERS | 1 + .../alerting/notifications/cue.mod/module.cue | 1 + apps/alerting/notifications/timeInterval.cue | 37 ++ .../feature-toggles/index.md | 1 + hack/README.md | 9 +- .../src/types/featureToggles.gen.ts | 1 + .../alerting_notifications/v0alpha1/doc.go | 6 + .../v0alpha1/register.go | 53 ++ .../v0alpha1/testing.go | 115 ++++ .../v0alpha1/timeinterval_spec.go | 38 ++ .../alerting_notifications/v0alpha1/types.go | 87 +++ .../v0alpha1/zz_generated.deepcopy.go | 157 +++++ .../v0alpha1/zz_generated.defaults.go | 19 + .../v0alpha1/zz_generated.openapi.go | 303 ++++++++++ ...enerated.openapi_violation_exceptions.list | 4 + .../v0alpha1/interval.go | 83 +++ .../v0alpha1/timeinterval.go | 196 +++++++ .../v0alpha1/timeintervalspec.go | 39 ++ .../v0alpha1/timerange.go | 34 ++ pkg/generated/applyconfiguration/utils.go | 26 +- .../clientset/versioned/clientset.go | 15 +- .../versioned/fake/clientset_generated.go | 7 + .../clientset/versioned/fake/register.go | 2 + .../clientset/versioned/scheme/register.go | 2 + .../v0alpha1/alerting_notifications_client.go | 93 +++ .../alerting_notifications/v0alpha1/doc.go | 6 + .../v0alpha1/fake/doc.go | 6 + .../fake_alerting_notifications_client.go | 26 + .../v0alpha1/fake/fake_timeinterval.go | 140 +++++ .../v0alpha1/generated_expansion.go | 7 + .../v0alpha1/timeinterval.go | 194 +++++++ .../alerting_notifications/interface.go | 32 ++ .../v0alpha1/interface.go | 31 + .../v0alpha1/timeinterval.go | 76 +++ .../informers/externalversions/factory.go | 6 + .../informers/externalversions/generic.go | 11 +- .../v0alpha1/expansion_generated.go | 13 + .../v0alpha1/timeinterval.go | 85 +++ .../apis/alerting/notifications/register.go | 112 ++++ .../notifications/timeinterval/authorize.go | 54 ++ .../notifications/timeinterval/conversions.go | 97 ++++ .../timeinterval/legacy_storage.go | 205 +++++++ .../notifications/timeinterval/storage.go | 79 +++ pkg/registry/apis/apis.go | 2 + pkg/registry/apis/wireset.go | 2 + pkg/services/accesscontrol/models.go | 5 +- pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 15 +- pkg/services/ngalert/ngalert.go | 8 +- .../timeinterval/timeinterval_test.go | 541 ++++++++++++++++++ pkg/tests/apis/helper.go | 109 ++-- 53 files changed, 3149 insertions(+), 54 deletions(-) create mode 100644 apps/alerting/notifications/cue.mod/module.cue create mode 100644 apps/alerting/notifications/timeInterval.cue create mode 100644 pkg/apis/alerting_notifications/v0alpha1/doc.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/register.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/testing.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/timeinterval_spec.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/types.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/zz_generated.defaults.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go create mode 100644 pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list create mode 100644 pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/interval.go create mode 100644 pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeinterval.go create mode 100644 pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeintervalspec.go create mode 100644 pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timerange.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/doc.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/doc.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_timeinterval.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go create mode 100644 pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/timeinterval.go create mode 100644 pkg/generated/informers/externalversions/alerting_notifications/interface.go create mode 100644 pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go create mode 100644 pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/timeinterval.go create mode 100644 pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go create mode 100644 pkg/generated/listers/alerting_notifications/v0alpha1/timeinterval.go create mode 100644 pkg/registry/apis/alerting/notifications/register.go create mode 100644 pkg/registry/apis/alerting/notifications/timeinterval/authorize.go create mode 100644 pkg/registry/apis/alerting/notifications/timeinterval/conversions.go create mode 100644 pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go create mode 100644 pkg/registry/apis/alerting/notifications/timeinterval/storage.go create mode 100644 pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a2fc88371ba..cf7bc7537ad 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -71,6 +71,7 @@ /scripts/modowners/ @grafana/grafana-backend-services-squad /hack/ @grafana/grafana-app-platform-squad +/apps/alerting/ @grafana/alerting-backend /pkg/api/ @grafana/grafana-backend-group /pkg/apis/ @grafana/grafana-app-platform-squad /pkg/bus/ @grafana/grafana-search-and-storage diff --git a/apps/alerting/notifications/cue.mod/module.cue b/apps/alerting/notifications/cue.mod/module.cue new file mode 100644 index 00000000000..d5f6f45c25e --- /dev/null +++ b/apps/alerting/notifications/cue.mod/module.cue @@ -0,0 +1 @@ +module:"notifications" \ No newline at end of file diff --git a/apps/alerting/notifications/timeInterval.cue b/apps/alerting/notifications/timeInterval.cue new file mode 100644 index 00000000000..04bff2c995e --- /dev/null +++ b/apps/alerting/notifications/timeInterval.cue @@ -0,0 +1,37 @@ +package core + +timeInterval: { + kind: "TimeInterval" + group: "notifications" + apiResource: { + groupOverride: "notifications.alerting.grafana.app" + } + codegen: { + frontend: false + backend: true + } + pluralName: "TimeIntervals" + current: "v0alpha1" + versions: { + "v0alpha1": { + schema: { + #TimeRange: { + start_time: string + end_time: string + } + #Interval: { + times?: [...#TimeRange] + weekdays?: [...string] + days_of_month?: [...string] + months?: [...string] + years?: [...string] + location?: string + } + spec: { + name: string + time_intervals: [...#Interval] + } + } + } + } +} \ No newline at end of file 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 5f6237ff724..567d4a087a0 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -191,6 +191,7 @@ Experimental features might be changed or removed without prior notice. | `pinNavItems` | Enables pinning of nav items | | `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | | `databaseReadReplica` | Use a read replica for some database queries. | +| `alertingApiServer` | Register Alerting APIs with the K8s API server | ## Development feature toggles diff --git a/hack/README.md b/hack/README.md index 5a8819a9db0..f425f5500d5 100644 --- a/hack/README.md +++ b/hack/README.md @@ -22,8 +22,13 @@ go mod download # the happy path ./hack/update-codegen.sh - +``` Note that the script deletes existing openapi go code and regenerates in place so that you will temporarily see deleted files in your `git status`. After a successful run, you should see them restored. -``` + +If resource client is not generated for your resource make sure that it follows the k8s guidelines for structuring the resource definition + +- the directory is named after resource version, i.e. `/v` (e.g. service/v0alpha1) +- the resource directory contains file `types.go` that includes resource definitions +- the resource definitions are annotated with comment `// +genclient` diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 91d4bcf2a65..12affb3258d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -198,4 +198,5 @@ export interface FeatureToggles { databaseReadReplica?: boolean; zanzana?: boolean; passScopeToDashboardApi?: boolean; + alertingApiServer?: boolean; } diff --git a/pkg/apis/alerting_notifications/v0alpha1/doc.go b/pkg/apis/alerting_notifications/v0alpha1/doc.go new file mode 100644 index 00000000000..f1e30a11e31 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/doc.go @@ -0,0 +1,6 @@ +// +k8s:deepcopy-gen=package +// +k8s:openapi-gen=true +// +k8s:defaulter-gen=TypeMeta +// +groupName=notifications.alerting.grafana.app + +package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" diff --git a/pkg/apis/alerting_notifications/v0alpha1/register.go b/pkg/apis/alerting_notifications/v0alpha1/register.go new file mode 100644 index 00000000000..a6be83e04ef --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/register.go @@ -0,0 +1,53 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +func init() { + localSchemeBuilder.Register(AddKnownTypes) +} + +const ( + GROUP = "notifications.alerting.grafana.app" + VERSION = "v0alpha1" + APIVERSION = GROUP + "/" + VERSION +) + +var ( + TimeIntervalResourceInfo = common.NewResourceInfo(GROUP, VERSION, + "timeintervals", "timeinterval", "TimeIntervals", + func() runtime.Object { return &TimeInterval{} }, + func() runtime.Object { return &TimeIntervalList{} }, + ) + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} + // SchemaBuilder is used by standard codegen + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func AddKnownTypes(scheme *runtime.Scheme) error { + return AddKnownTypesGroup(scheme, SchemeGroupVersion) +} + +// Adds the list of known types to the given scheme and group version. +func AddKnownTypesGroup(scheme *runtime.Scheme, g schema.GroupVersion) error { + scheme.AddKnownTypes(g, + &TimeInterval{}, + &TimeIntervalList{}, + ) + metav1.AddToGroupVersion(scheme, g) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/testing.go b/pkg/apis/alerting_notifications/v0alpha1/testing.go new file mode 100644 index 00000000000..355f7422f20 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/testing.go @@ -0,0 +1,115 @@ +package v0alpha1 + +import ( + "fmt" + "math/rand" + "slices" + "strings" + "time" + + "github.com/grafana/grafana/pkg/util" +) + +// +k8s:openapi-gen=false +// +k8s:deepcopy-gen=false +type IntervalMutator func(spec *Interval) + +// +k8s:openapi-gen=false +// +k8s:deepcopy-gen=false +type IntervalGenerator struct { + mutators []IntervalMutator +} + +func (t IntervalGenerator) With(mutators ...IntervalMutator) IntervalGenerator { + return IntervalGenerator{ + mutators: append(t.mutators, mutators...), + } +} + +func (t IntervalGenerator) generateDaysOfMonth() string { + isRange := rand.Int()%2 == 0 + if !isRange { + return fmt.Sprintf("%d", rand.Intn(30)+1) + } + from := rand.Intn(15) + 1 + to := rand.Intn(31-from) + from + 1 + return fmt.Sprintf("%d:%d", from, to) +} + +func (t IntervalGenerator) generateTimeRange() TimeRange { + from := rand.Int63n(1440 / 2) // [0, 719] + to := from + rand.Int63n(1440/2) + 1 // from < ([0,719] + [1,720]) < 1440 + return TimeRange{ + StartTime: time.Unix(from*60, 0).UTC().Format("15:04"), + EndTime: time.Unix(to*60, 0).UTC().Format("15:04"), + } +} + +func (t IntervalGenerator) generateWeekday() string { + day := rand.Intn(7) + return strings.ToLower(time.Weekday(day).String()) +} + +func (t IntervalGenerator) generateYear() string { + from := 1970 + rand.Intn(100) + if rand.Int()%3 == 0 { + to := 1970 + from + rand.Intn(10) + 1 + return fmt.Sprintf("%d:%d", from, to) + } + return fmt.Sprintf("%d", from) +} + +func (t IntervalGenerator) generateLocation() *string { + if rand.Int()%3 == 0 { + return nil + } + return util.Pointer("UTC") +} + +func (t IntervalGenerator) generateMonth() string { + return fmt.Sprintf("%d", rand.Intn(12)+1) +} + +func (t IntervalGenerator) GenerateMany(count int) []Interval { + result := make([]Interval, 0, count) + for i := 0; i < count; i++ { + result = append(result, t.Generate()) + } + return result +} + +func (t IntervalGenerator) Generate() Interval { + i := Interval{ + DaysOfMonth: generateMany(rand.Intn(6), true, t.generateDaysOfMonth), + Location: t.generateLocation(), + Months: generateMany(rand.Intn(3), true, t.generateMonth), + Times: generateMany(rand.Intn(6), true, t.generateTimeRange), + Weekdays: generateMany(rand.Intn(3), true, t.generateWeekday), + Years: generateMany(rand.Intn(3), true, t.generateYear), + } + for _, mutator := range t.mutators { + mutator(&i) + } + return i +} + +func generateMany[T comparable](repeatTimes int, unique bool, f func() T) []T { + qty := repeatTimes + 1 + result := make([]T, 0, qty) + for i := 0; i < qty; i++ { + r := f() + if unique && slices.Contains(result, r) { + continue + } + result = append(result, f()) + } + return result +} + +func CopyWith(in Interval, mutators ...IntervalMutator) Interval { + r := *in.DeepCopy() + for _, mut := range mutators { + mut(&r) + } + return r +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/timeinterval_spec.go b/pkg/apis/alerting_notifications/v0alpha1/timeinterval_spec.go new file mode 100644 index 00000000000..f345bda7416 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/timeinterval_spec.go @@ -0,0 +1,38 @@ +package v0alpha1 + +// Interval defines model for Interval. +// +k8s:openapi-gen=true +type Interval struct { + // +listType=atomic + DaysOfMonth []string `json:"days_of_month,omitempty"` + + // +listType=atomic + Location *string `json:"location,omitempty"` + + // +listType=atomic + Months []string `json:"months,omitempty"` + + // +listType=atomic + Times []TimeRange `json:"times,omitempty"` + + // +listType=atomic + Weekdays []string `json:"weekdays,omitempty"` + + // +listType=atomic + Years []string `json:"years,omitempty"` +} + +// Spec defines model for Spec. +// +k8s:openapi-gen=true +type TimeIntervalSpec struct { + Name string `json:"name"` + // +listType=atomic + TimeIntervals []Interval `json:"time_intervals"` +} + +// TimeRange defines model for TimeRange. +// +k8s:openapi-gen=true +type TimeRange struct { + EndTime string `json:"end_time"` + StartTime string `json:"start_time"` +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/types.go b/pkg/apis/alerting_notifications/v0alpha1/types.go new file mode 100644 index 00000000000..6a970ad4516 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/types.go @@ -0,0 +1,87 @@ +package v0alpha1 + +import ( + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type TimeInterval struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec TimeIntervalSpec `json:"spec"` +} + +func (o *TimeInterval) GetSpec() any { + return o.Spec +} + +func (o *TimeInterval) SetSpec(spec any) error { + cast, ok := spec.(TimeIntervalSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *TimeInterval) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *TimeInterval) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *TimeInterval) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *TimeInterval) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *TimeInterval) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *TimeInterval) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:openapi-gen=true +type TimeIntervalList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []TimeInterval `json:"items"` +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..918c3d81cb2 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.deepcopy.go @@ -0,0 +1,157 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Interval) DeepCopyInto(out *Interval) { + *out = *in + if in.DaysOfMonth != nil { + in, out := &in.DaysOfMonth, &out.DaysOfMonth + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Location != nil { + in, out := &in.Location, &out.Location + *out = new(string) + **out = **in + } + if in.Months != nil { + in, out := &in.Months, &out.Months + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Times != nil { + in, out := &in.Times, &out.Times + *out = make([]TimeRange, len(*in)) + copy(*out, *in) + } + if in.Weekdays != nil { + in, out := &in.Weekdays, &out.Weekdays + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Years != nil { + in, out := &in.Years, &out.Years + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Interval. +func (in *Interval) DeepCopy() *Interval { + if in == nil { + return nil + } + out := new(Interval) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TimeInterval) DeepCopyInto(out *TimeInterval) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeInterval. +func (in *TimeInterval) DeepCopy() *TimeInterval { + if in == nil { + return nil + } + out := new(TimeInterval) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TimeInterval) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TimeIntervalList) DeepCopyInto(out *TimeIntervalList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TimeInterval, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeIntervalList. +func (in *TimeIntervalList) DeepCopy() *TimeIntervalList { + if in == nil { + return nil + } + out := new(TimeIntervalList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TimeIntervalList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TimeIntervalSpec) DeepCopyInto(out *TimeIntervalSpec) { + *out = *in + if in.TimeIntervals != nil { + in, out := &in.TimeIntervals, &out.TimeIntervals + *out = make([]Interval, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeIntervalSpec. +func (in *TimeIntervalSpec) DeepCopy() *TimeIntervalSpec { + if in == nil { + return nil + } + out := new(TimeIntervalSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TimeRange) DeepCopyInto(out *TimeRange) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeRange. +func (in *TimeRange) DeepCopy() *TimeRange { + if in == nil { + return nil + } + out := new(TimeRange) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.defaults.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.defaults.go new file mode 100644 index 00000000000..238fc2f4edc --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.defaults.go @@ -0,0 +1,19 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go new file mode 100644 index 00000000000..8d5ff04f0a0 --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi.go @@ -0,0 +1,303 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by openapi-gen. DO NOT EDIT. + +// This file was autogenerated by openapi-gen. Do not edit it manually! + +package v0alpha1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval": schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval": schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalList": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalList(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec": schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalSpec(ref), + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange": schema_pkg_apis_alerting_notifications_v0alpha1_TimeRange(ref), + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_Interval(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Interval defines model for Interval.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "days_of_month": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "location": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "months": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "times": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange"), + }, + }, + }, + }, + }, + "weekdays": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "years": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeRange"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_TimeInterval(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeIntervalSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "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", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "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", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.TimeInterval", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_TimeIntervalSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Spec defines model for Spec.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "time_intervals": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval"), + }, + }, + }, + }, + }, + }, + Required: []string{"name", "time_intervals"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1.Interval"}, + } +} + +func schema_pkg_apis_alerting_notifications_v0alpha1_TimeRange(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TimeRange defines model for TimeRange.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "end_time": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "start_time": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"end_time", "start_time"}, + }, + }, + } +} diff --git a/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list new file mode 100644 index 00000000000..7b756e64dfd --- /dev/null +++ b/pkg/apis/alerting_notifications/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -0,0 +1,4 @@ +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,Interval,DaysOfMonth +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeIntervalSpec,TimeIntervals +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,EndTime +API rule violation: names_match,github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1,TimeRange,StartTime diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/interval.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/interval.go new file mode 100644 index 00000000000..951000054bb --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/interval.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// IntervalApplyConfiguration represents an declarative configuration of the Interval type for use +// with apply. +type IntervalApplyConfiguration struct { + DaysOfMonth []string `json:"days_of_month,omitempty"` + Location *string `json:"location,omitempty"` + Months []string `json:"months,omitempty"` + Times []TimeRangeApplyConfiguration `json:"times,omitempty"` + Weekdays []string `json:"weekdays,omitempty"` + Years []string `json:"years,omitempty"` +} + +// IntervalApplyConfiguration constructs an declarative configuration of the Interval type for use with +// apply. +func Interval() *IntervalApplyConfiguration { + return &IntervalApplyConfiguration{} +} + +// WithDaysOfMonth adds the given value to the DaysOfMonth field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the DaysOfMonth field. +func (b *IntervalApplyConfiguration) WithDaysOfMonth(values ...string) *IntervalApplyConfiguration { + for i := range values { + b.DaysOfMonth = append(b.DaysOfMonth, values[i]) + } + return b +} + +// WithLocation sets the Location field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Location field is set to the value of the last call. +func (b *IntervalApplyConfiguration) WithLocation(value string) *IntervalApplyConfiguration { + b.Location = &value + return b +} + +// WithMonths adds the given value to the Months field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Months field. +func (b *IntervalApplyConfiguration) WithMonths(values ...string) *IntervalApplyConfiguration { + for i := range values { + b.Months = append(b.Months, values[i]) + } + return b +} + +// WithTimes adds the given value to the Times field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Times field. +func (b *IntervalApplyConfiguration) WithTimes(values ...*TimeRangeApplyConfiguration) *IntervalApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithTimes") + } + b.Times = append(b.Times, *values[i]) + } + return b +} + +// WithWeekdays adds the given value to the Weekdays field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Weekdays field. +func (b *IntervalApplyConfiguration) WithWeekdays(values ...string) *IntervalApplyConfiguration { + for i := range values { + b.Weekdays = append(b.Weekdays, values[i]) + } + return b +} + +// WithYears adds the given value to the Years field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Years field. +func (b *IntervalApplyConfiguration) WithYears(values ...string) *IntervalApplyConfiguration { + for i := range values { + b.Years = append(b.Years, values[i]) + } + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeinterval.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeinterval.go new file mode 100644 index 00000000000..f94c6e8f95f --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeinterval.go @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// TimeIntervalApplyConfiguration represents an declarative configuration of the TimeInterval type for use +// with apply. +type TimeIntervalApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *TimeIntervalSpecApplyConfiguration `json:"spec,omitempty"` +} + +// TimeInterval constructs an declarative configuration of the TimeInterval type for use with +// apply. +func TimeInterval(name, namespace string) *TimeIntervalApplyConfiguration { + b := &TimeIntervalApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("TimeInterval") + b.WithAPIVersion("notifications.alerting.grafana.app/v0alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithKind(value string) *TimeIntervalApplyConfiguration { + b.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithAPIVersion(value string) *TimeIntervalApplyConfiguration { + b.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithName(value string) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithGenerateName(value string) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithNamespace(value string) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithUID(value types.UID) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithResourceVersion(value string) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithGeneration(value int64) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithCreationTimestamp(value metav1.Time) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *TimeIntervalApplyConfiguration) WithLabels(entries map[string]string) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *TimeIntervalApplyConfiguration) WithAnnotations(entries map[string]string) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *TimeIntervalApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.OwnerReferences = append(b.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *TimeIntervalApplyConfiguration) WithFinalizers(values ...string) *TimeIntervalApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.Finalizers = append(b.Finalizers, values[i]) + } + return b +} + +func (b *TimeIntervalApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *TimeIntervalApplyConfiguration) WithSpec(value *TimeIntervalSpecApplyConfiguration) *TimeIntervalApplyConfiguration { + b.Spec = value + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeintervalspec.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeintervalspec.go new file mode 100644 index 00000000000..ba3b255ed33 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timeintervalspec.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// TimeIntervalSpecApplyConfiguration represents an declarative configuration of the TimeIntervalSpec type for use +// with apply. +type TimeIntervalSpecApplyConfiguration struct { + Name *string `json:"name,omitempty"` + TimeIntervals []IntervalApplyConfiguration `json:"time_intervals,omitempty"` +} + +// TimeIntervalSpecApplyConfiguration constructs an declarative configuration of the TimeIntervalSpec type for use with +// apply. +func TimeIntervalSpec() *TimeIntervalSpecApplyConfiguration { + return &TimeIntervalSpecApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *TimeIntervalSpecApplyConfiguration) WithName(value string) *TimeIntervalSpecApplyConfiguration { + b.Name = &value + return b +} + +// WithTimeIntervals adds the given value to the TimeIntervals field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the TimeIntervals field. +func (b *TimeIntervalSpecApplyConfiguration) WithTimeIntervals(values ...*IntervalApplyConfiguration) *TimeIntervalSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithTimeIntervals") + } + b.TimeIntervals = append(b.TimeIntervals, *values[i]) + } + return b +} diff --git a/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timerange.go b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timerange.go new file mode 100644 index 00000000000..77242ddc317 --- /dev/null +++ b/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1/timerange.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v0alpha1 + +// TimeRangeApplyConfiguration represents an declarative configuration of the TimeRange type for use +// with apply. +type TimeRangeApplyConfiguration struct { + EndTime *string `json:"end_time,omitempty"` + StartTime *string `json:"start_time,omitempty"` +} + +// TimeRangeApplyConfiguration constructs an declarative configuration of the TimeRange type for use with +// apply. +func TimeRange() *TimeRangeApplyConfiguration { + return &TimeRangeApplyConfiguration{} +} + +// WithEndTime sets the EndTime field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the EndTime field is set to the value of the last call. +func (b *TimeRangeApplyConfiguration) WithEndTime(value string) *TimeRangeApplyConfiguration { + b.EndTime = &value + return b +} + +// WithStartTime sets the StartTime field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the StartTime field is set to the value of the last call. +func (b *TimeRangeApplyConfiguration) WithStartTime(value string) *TimeRangeApplyConfiguration { + b.StartTime = &value + return b +} diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go index 93ca019455c..561bf3385e5 100644 --- a/pkg/generated/applyconfiguration/utils.go +++ b/pkg/generated/applyconfiguration/utils.go @@ -5,8 +5,10 @@ package applyconfiguration import ( - v0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" - servicev0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/service/v0alpha1" + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + applyconfigurationservicev0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/service/v0alpha1" schema "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -14,11 +16,21 @@ import ( // apply configuration type exists for the given GroupVersionKind. func ForKind(kind schema.GroupVersionKind) interface{} { switch kind { - // Group=service.grafana.app, Version=v0alpha1 - case v0alpha1.SchemeGroupVersion.WithKind("ExternalName"): - return &servicev0alpha1.ExternalNameApplyConfiguration{} - case v0alpha1.SchemeGroupVersion.WithKind("ExternalNameSpec"): - return &servicev0alpha1.ExternalNameSpecApplyConfiguration{} + // Group=notifications.alerting.grafana.app, Version=v0alpha1 + case v0alpha1.SchemeGroupVersion.WithKind("Interval"): + return &alertingnotificationsv0alpha1.IntervalApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("TimeInterval"): + return &alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("TimeIntervalSpec"): + return &alertingnotificationsv0alpha1.TimeIntervalSpecApplyConfiguration{} + case v0alpha1.SchemeGroupVersion.WithKind("TimeRange"): + return &alertingnotificationsv0alpha1.TimeRangeApplyConfiguration{} + + // Group=service.grafana.app, Version=v0alpha1 + case servicev0alpha1.SchemeGroupVersion.WithKind("ExternalName"): + return &applyconfigurationservicev0alpha1.ExternalNameApplyConfiguration{} + case servicev0alpha1.SchemeGroupVersion.WithKind("ExternalNameSpec"): + return &applyconfigurationservicev0alpha1.ExternalNameSpecApplyConfiguration{} } return nil diff --git a/pkg/generated/clientset/versioned/clientset.go b/pkg/generated/clientset/versioned/clientset.go index 55fa581701a..dc02a208e84 100644 --- a/pkg/generated/clientset/versioned/clientset.go +++ b/pkg/generated/clientset/versioned/clientset.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" + notificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1" servicev0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/service/v0alpha1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" @@ -16,13 +17,20 @@ import ( type Interface interface { Discovery() discovery.DiscoveryInterface + NotificationsV0alpha1() notificationsv0alpha1.NotificationsV0alpha1Interface ServiceV0alpha1() servicev0alpha1.ServiceV0alpha1Interface } // Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient - serviceV0alpha1 *servicev0alpha1.ServiceV0alpha1Client + notificationsV0alpha1 *notificationsv0alpha1.NotificationsV0alpha1Client + serviceV0alpha1 *servicev0alpha1.ServiceV0alpha1Client +} + +// NotificationsV0alpha1 retrieves the NotificationsV0alpha1Client +func (c *Clientset) NotificationsV0alpha1() notificationsv0alpha1.NotificationsV0alpha1Interface { + return c.notificationsV0alpha1 } // ServiceV0alpha1 retrieves the ServiceV0alpha1Client @@ -74,6 +82,10 @@ func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, var cs Clientset var err error + cs.notificationsV0alpha1, err = notificationsv0alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } cs.serviceV0alpha1, err = servicev0alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err @@ -99,6 +111,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset + cs.notificationsV0alpha1 = notificationsv0alpha1.New(c) cs.serviceV0alpha1 = servicev0alpha1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) diff --git a/pkg/generated/clientset/versioned/fake/clientset_generated.go b/pkg/generated/clientset/versioned/fake/clientset_generated.go index c7e19bcc918..5b7771eb90a 100644 --- a/pkg/generated/clientset/versioned/fake/clientset_generated.go +++ b/pkg/generated/clientset/versioned/fake/clientset_generated.go @@ -6,6 +6,8 @@ package fake import ( clientset "github.com/grafana/grafana/pkg/generated/clientset/versioned" + notificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1" + fakenotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake" servicev0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/service/v0alpha1" fakeservicev0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/service/v0alpha1/fake" "k8s.io/apimachinery/pkg/runtime" @@ -65,6 +67,11 @@ var ( _ testing.FakeClient = &Clientset{} ) +// NotificationsV0alpha1 retrieves the NotificationsV0alpha1Client +func (c *Clientset) NotificationsV0alpha1() notificationsv0alpha1.NotificationsV0alpha1Interface { + return &fakenotificationsv0alpha1.FakeNotificationsV0alpha1{Fake: &c.Fake} +} + // ServiceV0alpha1 retrieves the ServiceV0alpha1Client func (c *Clientset) ServiceV0alpha1() servicev0alpha1.ServiceV0alpha1Interface { return &fakeservicev0alpha1.FakeServiceV0alpha1{Fake: &c.Fake} diff --git a/pkg/generated/clientset/versioned/fake/register.go b/pkg/generated/clientset/versioned/fake/register.go index bbf12658e76..00ae96dd36e 100644 --- a/pkg/generated/clientset/versioned/fake/register.go +++ b/pkg/generated/clientset/versioned/fake/register.go @@ -5,6 +5,7 @@ package fake import ( + notificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -17,6 +18,7 @@ var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ + notificationsv0alpha1.AddToScheme, servicev0alpha1.AddToScheme, } diff --git a/pkg/generated/clientset/versioned/scheme/register.go b/pkg/generated/clientset/versioned/scheme/register.go index dd6e9619160..6ae4e0deec1 100644 --- a/pkg/generated/clientset/versioned/scheme/register.go +++ b/pkg/generated/clientset/versioned/scheme/register.go @@ -5,6 +5,7 @@ package scheme import ( + notificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -17,6 +18,7 @@ var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ + notificationsv0alpha1.AddToScheme, servicev0alpha1.AddToScheme, } diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go new file mode 100644 index 00000000000..5c81a66d6ec --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/alerting_notifications_client.go @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "net/http" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/generated/clientset/versioned/scheme" + rest "k8s.io/client-go/rest" +) + +type NotificationsV0alpha1Interface interface { + RESTClient() rest.Interface + TimeIntervalsGetter +} + +// NotificationsV0alpha1Client is used to interact with features provided by the notifications.alerting.grafana.app group. +type NotificationsV0alpha1Client struct { + restClient rest.Interface +} + +func (c *NotificationsV0alpha1Client) TimeIntervals(namespace string) TimeIntervalInterface { + return newTimeIntervals(c, namespace) +} + +// NewForConfig creates a new NotificationsV0alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*NotificationsV0alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new NotificationsV0alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*NotificationsV0alpha1Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &NotificationsV0alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new NotificationsV0alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *NotificationsV0alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new NotificationsV0alpha1Client for the given RESTClient. +func New(c rest.Interface) *NotificationsV0alpha1Client { + return &NotificationsV0alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v0alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *NotificationsV0alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/doc.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/doc.go new file mode 100644 index 00000000000..1c86744fecc --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/doc.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v0alpha1 diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/doc.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/doc.go new file mode 100644 index 00000000000..d96b985b3ea --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/doc.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go new file mode 100644 index 00000000000..ece678250d8 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_alerting_notifications_client.go @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeNotificationsV0alpha1 struct { + *testing.Fake +} + +func (c *FakeNotificationsV0alpha1) TimeIntervals(namespace string) v0alpha1.TimeIntervalInterface { + return &FakeTimeIntervals{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeNotificationsV0alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_timeinterval.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_timeinterval.go new file mode 100644 index 00000000000..569c83a433d --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/fake/fake_timeinterval.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + json "encoding/json" + "fmt" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeTimeIntervals implements TimeIntervalInterface +type FakeTimeIntervals struct { + Fake *FakeNotificationsV0alpha1 + ns string +} + +var timeintervalsResource = v0alpha1.SchemeGroupVersion.WithResource("timeintervals") + +var timeintervalsKind = v0alpha1.SchemeGroupVersion.WithKind("TimeInterval") + +// Get takes name of the timeInterval, and returns the corresponding timeInterval object, and an error if there is any. +func (c *FakeTimeIntervals) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.TimeInterval, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(timeintervalsResource, c.ns, name), &v0alpha1.TimeInterval{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.TimeInterval), err +} + +// List takes label and field selectors, and returns the list of TimeIntervals that match those selectors. +func (c *FakeTimeIntervals) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.TimeIntervalList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(timeintervalsResource, timeintervalsKind, c.ns, opts), &v0alpha1.TimeIntervalList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v0alpha1.TimeIntervalList{ListMeta: obj.(*v0alpha1.TimeIntervalList).ListMeta} + for _, item := range obj.(*v0alpha1.TimeIntervalList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested timeIntervals. +func (c *FakeTimeIntervals) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(timeintervalsResource, c.ns, opts)) + +} + +// Create takes the representation of a timeInterval and creates it. Returns the server's representation of the timeInterval, and an error, if there is any. +func (c *FakeTimeIntervals) Create(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.CreateOptions) (result *v0alpha1.TimeInterval, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(timeintervalsResource, c.ns, timeInterval), &v0alpha1.TimeInterval{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.TimeInterval), err +} + +// Update takes the representation of a timeInterval and updates it. Returns the server's representation of the timeInterval, and an error, if there is any. +func (c *FakeTimeIntervals) Update(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.UpdateOptions) (result *v0alpha1.TimeInterval, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(timeintervalsResource, c.ns, timeInterval), &v0alpha1.TimeInterval{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.TimeInterval), err +} + +// Delete takes name of the timeInterval and deletes it. Returns an error if one occurs. +func (c *FakeTimeIntervals) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(timeintervalsResource, c.ns, name, opts), &v0alpha1.TimeInterval{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeTimeIntervals) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(timeintervalsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v0alpha1.TimeIntervalList{}) + return err +} + +// Patch applies the patch and returns the patched timeInterval. +func (c *FakeTimeIntervals) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.TimeInterval, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(timeintervalsResource, c.ns, name, pt, data, subresources...), &v0alpha1.TimeInterval{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.TimeInterval), err +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied timeInterval. +func (c *FakeTimeIntervals) Apply(ctx context.Context, timeInterval *alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TimeInterval, err error) { + if timeInterval == nil { + return nil, fmt.Errorf("timeInterval provided to Apply must not be nil") + } + data, err := json.Marshal(timeInterval) + if err != nil { + return nil, err + } + name := timeInterval.Name + if name == nil { + return nil, fmt.Errorf("timeInterval.Name must be provided to Apply") + } + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(timeintervalsResource, c.ns, *name, types.ApplyPatchType, data), &v0alpha1.TimeInterval{}) + + if obj == nil { + return nil, err + } + return obj.(*v0alpha1.TimeInterval), err +} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go new file mode 100644 index 00000000000..cf6e218f4ae --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/generated_expansion.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package v0alpha1 + +type TimeIntervalExpansion interface{} diff --git a/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/timeinterval.go b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/timeinterval.go new file mode 100644 index 00000000000..cc3b2c05bd1 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1/timeinterval.go @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by client-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + json "encoding/json" + "fmt" + "time" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/applyconfiguration/alerting_notifications/v0alpha1" + scheme "github.com/grafana/grafana/pkg/generated/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// TimeIntervalsGetter has a method to return a TimeIntervalInterface. +// A group's client should implement this interface. +type TimeIntervalsGetter interface { + TimeIntervals(namespace string) TimeIntervalInterface +} + +// TimeIntervalInterface has methods to work with TimeInterval resources. +type TimeIntervalInterface interface { + Create(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.CreateOptions) (*v0alpha1.TimeInterval, error) + Update(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.UpdateOptions) (*v0alpha1.TimeInterval, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v0alpha1.TimeInterval, error) + List(ctx context.Context, opts v1.ListOptions) (*v0alpha1.TimeIntervalList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.TimeInterval, err error) + Apply(ctx context.Context, timeInterval *alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TimeInterval, err error) + TimeIntervalExpansion +} + +// timeIntervals implements TimeIntervalInterface +type timeIntervals struct { + client rest.Interface + ns string +} + +// newTimeIntervals returns a TimeIntervals +func newTimeIntervals(c *NotificationsV0alpha1Client, namespace string) *timeIntervals { + return &timeIntervals{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the timeInterval, and returns the corresponding timeInterval object, and an error if there is any. +func (c *timeIntervals) Get(ctx context.Context, name string, options v1.GetOptions) (result *v0alpha1.TimeInterval, err error) { + result = &v0alpha1.TimeInterval{} + err = c.client.Get(). + Namespace(c.ns). + Resource("timeintervals"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of TimeIntervals that match those selectors. +func (c *timeIntervals) List(ctx context.Context, opts v1.ListOptions) (result *v0alpha1.TimeIntervalList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v0alpha1.TimeIntervalList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("timeintervals"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested timeIntervals. +func (c *timeIntervals) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("timeintervals"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a timeInterval and creates it. Returns the server's representation of the timeInterval, and an error, if there is any. +func (c *timeIntervals) Create(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.CreateOptions) (result *v0alpha1.TimeInterval, err error) { + result = &v0alpha1.TimeInterval{} + err = c.client.Post(). + Namespace(c.ns). + Resource("timeintervals"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(timeInterval). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a timeInterval and updates it. Returns the server's representation of the timeInterval, and an error, if there is any. +func (c *timeIntervals) Update(ctx context.Context, timeInterval *v0alpha1.TimeInterval, opts v1.UpdateOptions) (result *v0alpha1.TimeInterval, err error) { + result = &v0alpha1.TimeInterval{} + err = c.client.Put(). + Namespace(c.ns). + Resource("timeintervals"). + Name(timeInterval.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(timeInterval). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the timeInterval and deletes it. Returns an error if one occurs. +func (c *timeIntervals) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("timeintervals"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *timeIntervals) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("timeintervals"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched timeInterval. +func (c *timeIntervals) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v0alpha1.TimeInterval, err error) { + result = &v0alpha1.TimeInterval{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("timeintervals"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} + +// Apply takes the given apply declarative configuration, applies it and returns the applied timeInterval. +func (c *timeIntervals) Apply(ctx context.Context, timeInterval *alertingnotificationsv0alpha1.TimeIntervalApplyConfiguration, opts v1.ApplyOptions) (result *v0alpha1.TimeInterval, err error) { + if timeInterval == nil { + return nil, fmt.Errorf("timeInterval provided to Apply must not be nil") + } + patchOpts := opts.ToPatchOptions() + data, err := json.Marshal(timeInterval) + if err != nil { + return nil, err + } + name := timeInterval.Name + if name == nil { + return nil, fmt.Errorf("timeInterval.Name must be provided to Apply") + } + result = &v0alpha1.TimeInterval{} + err = c.client.Patch(types.ApplyPatchType). + Namespace(c.ns). + Resource("timeintervals"). + Name(*name). + VersionedParams(&patchOpts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/interface.go b/pkg/generated/informers/externalversions/alerting_notifications/interface.go new file mode 100644 index 00000000000..631d480cd39 --- /dev/null +++ b/pkg/generated/informers/externalversions/alerting_notifications/interface.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by informer-gen. DO NOT EDIT. + +package alerting_notifications + +import ( + v0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1" + internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V0alpha1 provides access to shared informers for resources in V0alpha1. + V0alpha1() v0alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V0alpha1 returns a new v0alpha1.Interface. +func (g *group) V0alpha1() v0alpha1.Interface { + return v0alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go new file mode 100644 index 00000000000..2095a04954a --- /dev/null +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/interface.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by informer-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // TimeIntervals returns a TimeIntervalInformer. + TimeIntervals() TimeIntervalInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// TimeIntervals returns a TimeIntervalInformer. +func (v *version) TimeIntervals() TimeIntervalInformer { + return &timeIntervalInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/timeinterval.go b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/timeinterval.go new file mode 100644 index 00000000000..903bb8eb7bc --- /dev/null +++ b/pkg/generated/informers/externalversions/alerting_notifications/v0alpha1/timeinterval.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by informer-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + "context" + time "time" + + alertingnotificationsv0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + versioned "github.com/grafana/grafana/pkg/generated/clientset/versioned" + internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces" + v0alpha1 "github.com/grafana/grafana/pkg/generated/listers/alerting_notifications/v0alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// TimeIntervalInformer provides access to a shared informer and lister for +// TimeIntervals. +type TimeIntervalInformer interface { + Informer() cache.SharedIndexInformer + Lister() v0alpha1.TimeIntervalLister +} + +type timeIntervalInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewTimeIntervalInformer constructs a new informer for TimeInterval type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTimeIntervalInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredTimeIntervalInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredTimeIntervalInformer constructs a new informer for TimeInterval type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredTimeIntervalInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().TimeIntervals(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.NotificationsV0alpha1().TimeIntervals(namespace).Watch(context.TODO(), options) + }, + }, + &alertingnotificationsv0alpha1.TimeInterval{}, + resyncPeriod, + indexers, + ) +} + +func (f *timeIntervalInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredTimeIntervalInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *timeIntervalInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&alertingnotificationsv0alpha1.TimeInterval{}, f.defaultInformer) +} + +func (f *timeIntervalInformer) Lister() v0alpha1.TimeIntervalLister { + return v0alpha1.NewTimeIntervalLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/informers/externalversions/factory.go b/pkg/generated/informers/externalversions/factory.go index faca5c61edb..1c28112c022 100644 --- a/pkg/generated/informers/externalversions/factory.go +++ b/pkg/generated/informers/externalversions/factory.go @@ -10,6 +10,7 @@ import ( time "time" versioned "github.com/grafana/grafana/pkg/generated/clientset/versioned" + alertingnotifications "github.com/grafana/grafana/pkg/generated/informers/externalversions/alerting_notifications" internalinterfaces "github.com/grafana/grafana/pkg/generated/informers/externalversions/internalinterfaces" service "github.com/grafana/grafana/pkg/generated/informers/externalversions/service" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -239,9 +240,14 @@ type SharedInformerFactory interface { // client. InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer + Notifications() alertingnotifications.Interface Service() service.Interface } +func (f *sharedInformerFactory) Notifications() alertingnotifications.Interface { + return alertingnotifications.New(f, f.namespace, f.tweakListOptions) +} + func (f *sharedInformerFactory) Service() service.Interface { return service.New(f, f.namespace, f.tweakListOptions) } diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index 88cc30aa0f9..137fbb60117 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -7,7 +7,8 @@ package externalversions import ( "fmt" - v0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) @@ -38,8 +39,12 @@ func (f *genericInformer) Lister() cache.GenericLister { // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { - // Group=service.grafana.app, Version=v0alpha1 - case v0alpha1.SchemeGroupVersion.WithResource("externalnames"): + // Group=notifications.alerting.grafana.app, Version=v0alpha1 + case v0alpha1.SchemeGroupVersion.WithResource("timeintervals"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Notifications().V0alpha1().TimeIntervals().Informer()}, nil + + // Group=service.grafana.app, Version=v0alpha1 + case servicev0alpha1.SchemeGroupVersion.WithResource("externalnames"): return &genericInformer{resource: resource.GroupResource(), informer: f.Service().V0alpha1().ExternalNames().Informer()}, nil } diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go new file mode 100644 index 00000000000..65bad473129 --- /dev/null +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/expansion_generated.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by lister-gen. DO NOT EDIT. + +package v0alpha1 + +// TimeIntervalListerExpansion allows custom methods to be added to +// TimeIntervalLister. +type TimeIntervalListerExpansion interface{} + +// TimeIntervalNamespaceListerExpansion allows custom methods to be added to +// TimeIntervalNamespaceLister. +type TimeIntervalNamespaceListerExpansion interface{} diff --git a/pkg/generated/listers/alerting_notifications/v0alpha1/timeinterval.go b/pkg/generated/listers/alerting_notifications/v0alpha1/timeinterval.go new file mode 100644 index 00000000000..225a64b84e5 --- /dev/null +++ b/pkg/generated/listers/alerting_notifications/v0alpha1/timeinterval.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by lister-gen. DO NOT EDIT. + +package v0alpha1 + +import ( + v0alpha1 "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// TimeIntervalLister helps list TimeIntervals. +// All objects returned here must be treated as read-only. +type TimeIntervalLister interface { + // List lists all TimeIntervals in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error) + // TimeIntervals returns an object that can list and get TimeIntervals. + TimeIntervals(namespace string) TimeIntervalNamespaceLister + TimeIntervalListerExpansion +} + +// timeIntervalLister implements the TimeIntervalLister interface. +type timeIntervalLister struct { + indexer cache.Indexer +} + +// NewTimeIntervalLister returns a new TimeIntervalLister. +func NewTimeIntervalLister(indexer cache.Indexer) TimeIntervalLister { + return &timeIntervalLister{indexer: indexer} +} + +// List lists all TimeIntervals in the indexer. +func (s *timeIntervalLister) List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v0alpha1.TimeInterval)) + }) + return ret, err +} + +// TimeIntervals returns an object that can list and get TimeIntervals. +func (s *timeIntervalLister) TimeIntervals(namespace string) TimeIntervalNamespaceLister { + return timeIntervalNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// TimeIntervalNamespaceLister helps list and get TimeIntervals. +// All objects returned here must be treated as read-only. +type TimeIntervalNamespaceLister interface { + // List lists all TimeIntervals in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error) + // Get retrieves the TimeInterval from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v0alpha1.TimeInterval, error) + TimeIntervalNamespaceListerExpansion +} + +// timeIntervalNamespaceLister implements the TimeIntervalNamespaceLister +// interface. +type timeIntervalNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all TimeIntervals in the indexer for a given namespace. +func (s timeIntervalNamespaceLister) List(selector labels.Selector) (ret []*v0alpha1.TimeInterval, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v0alpha1.TimeInterval)) + }) + return ret, err +} + +// Get retrieves the TimeInterval from the indexer for a given namespace and name. +func (s timeIntervalNamespaceLister) Get(name string) (*v0alpha1.TimeInterval, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v0alpha1.Resource("timeinterval"), name) + } + return obj.(*v0alpha1.TimeInterval), nil +} diff --git a/pkg/registry/apis/alerting/notifications/register.go b/pkg/registry/apis/alerting/notifications/register.go new file mode 100644 index 00000000000..cb09efaa456 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/register.go @@ -0,0 +1,112 @@ +package notifications + +import ( + "context" + "fmt" + + "github.com/prometheus/client_golang/prometheus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/kube-openapi/pkg/common" + + notificationsModels "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/apiserver/builder" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + timeInterval "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ngalert" + "github.com/grafana/grafana/pkg/setting" +) + +var _ builder.APIGroupBuilder = (*NotificationsAPIBuilder)(nil) + +// This is used just so wire has something unique to return +type NotificationsAPIBuilder struct { + authz accesscontrol.AccessControl + ng *ngalert.AlertNG + namespacer request.NamespaceMapper + gv schema.GroupVersion +} + +func (t NotificationsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, toMode map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode { + // Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go + return grafanarest.Mode0 +} + +func RegisterAPIService( + features featuremgmt.FeatureToggles, + apiregistration builder.APIRegistrar, + cfg *setting.Cfg, + ng *ngalert.AlertNG, +) *NotificationsAPIBuilder { + if ng.IsDisabled() || !features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { + return nil + } + builder := &NotificationsAPIBuilder{ + ng: ng, + namespacer: request.GetNamespaceMapper(cfg), + gv: notificationsModels.SchemeGroupVersion, + authz: ng.Api.AccessControl, + } + apiregistration.RegisterAPI(builder) + return builder +} + +func (t NotificationsAPIBuilder) GetGroupVersion() schema.GroupVersion { + return t.gv +} + +func (t NotificationsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + err := notificationsModels.AddToScheme(scheme) + if err != nil { + return err + } + return scheme.SetVersionPriority(notificationsModels.SchemeGroupVersion) +} + +func (t NotificationsAPIBuilder) GetAPIGroupInfo( + scheme *runtime.Scheme, + codecs serializer.CodecFactory, + optsGetter generic.RESTOptionsGetter, + desiredMode grafanarest.DualWriterMode, + reg prometheus.Registerer, +) (*genericapiserver.APIGroupInfo, error) { + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(notificationsModels.GROUP, scheme, metav1.ParameterCodec, codecs) + + intervals, err := timeInterval.NewStorage(t.ng.Api.MuteTimings, t.namespacer, scheme, desiredMode, optsGetter, reg) + if err != nil { + return nil, fmt.Errorf("failed to initialize time-interval storage: %w", err) + } + + apiGroupInfo.VersionedResourcesStorageMap[notificationsModels.VERSION] = map[string]rest.Storage{ + notificationsModels.TimeIntervalResourceInfo.StoragePath(): intervals, + } + return &apiGroupInfo, nil +} + +func (t NotificationsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return notificationsModels.GetOpenAPIDefinitions +} + +func (t NotificationsAPIBuilder) GetAPIRoutes() *builder.APIRoutes { + return nil +} + +func (t NotificationsAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc( + func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + switch a.GetResource() { + case notificationsModels.TimeIntervalResourceInfo.GroupResource().Resource: + return timeInterval.Authorize(ctx, t.authz, a) + } + return authorizer.DecisionNoOpinion, "", nil + }) +} diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/authorize.go b/pkg/registry/apis/alerting/notifications/timeinterval/authorize.go new file mode 100644 index 00000000000..b5ccd9d9812 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/timeinterval/authorize.go @@ -0,0 +1,54 @@ +package timeinterval + +import ( + "context" + + "k8s.io/apiserver/pkg/authorization/authorizer" + + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + if attr.GetResource() != resourceInfo.GroupResource().Resource { + return authorizer.DecisionNoOpinion, "", nil + } + user, err := appcontext.User(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + var action accesscontrol.Evaluator + switch attr.GetVerb() { + case "patch": + fallthrough + case "create": + fallthrough + case "update": + action = accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), + ) + case "deletecollection": + fallthrough + case "delete": + action = accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), + ) + } + + eval := accesscontrol.EvalAny( + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsTimeIntervalsRead), + accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsRead), + ) + if action != nil { + eval = accesscontrol.EvalAll(eval, action) + } + + ok, err := ac.Evaluate(ctx, user, eval) + if ok { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionDeny, "", err +} diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/conversions.go b/pkg/registry/apis/alerting/notifications/timeinterval/conversions.go new file mode 100644 index 00000000000..cacb6f4369a --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/timeinterval/conversions.go @@ -0,0 +1,97 @@ +package timeinterval + +import ( + "encoding/json" + "fmt" + "hash/fnv" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +func getIntervalUID(t definitions.MuteTimeInterval) string { + sum := fnv.New64() + _, _ = sum.Write([]byte(t.Name)) + return fmt.Sprintf("%016x", sum.Sum64()) +} + +func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeIntervalList, error) { + data, err := json.Marshal(intervals) + if err != nil { + return nil, err + } + var specs []model.TimeIntervalSpec + err = json.Unmarshal(data, &specs) + if err != nil { + return nil, err + } + result := &model.TimeIntervalList{} + for idx := range specs { + interval := intervals[idx] + spec := specs[idx] + uid := getIntervalUID(interval) // TODO replace to stable UID when we switch to normal storage + result.Items = append(result.Items, model.TimeInterval{ + TypeMeta: resourceInfo.TypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(uid), // TODO This is needed to make PATCH work + Name: uid, // TODO replace to stable UID when we switch to normal storage + Namespace: namespacer(orgID), + Annotations: map[string]string{ // TODO find a better place for provenance? + "grafana.com/provenance": string(interval.Provenance), + }, + ResourceVersion: interval.Version, + }, + Spec: spec, + }) + } + return result, nil +} + +func convertToK8sResource(orgID int64, interval definitions.MuteTimeInterval, namespacer request.NamespaceMapper) (*model.TimeInterval, error) { + data, err := json.Marshal(interval) + if err != nil { + return nil, err + } + spec := model.TimeIntervalSpec{} + err = json.Unmarshal(data, &spec) + if err != nil { + return nil, err + } + + uid := getIntervalUID(interval) // TODO replace to stable UID when we switch to normal storage + return &model.TimeInterval{ + TypeMeta: resourceInfo.TypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(uid), // TODO This is needed to make PATCH work + Name: uid, // TODO replace to stable UID when we switch to normal storage + Namespace: namespacer(orgID), + Annotations: map[string]string{ // TODO find a better place for provenance? + "grafana.com/provenance": string(interval.Provenance), + }, + ResourceVersion: interval.Version, + }, + Spec: spec, + }, nil +} + +func convertToDomainModel(interval *model.TimeInterval) (definitions.MuteTimeInterval, error) { + b, err := json.Marshal(interval.Spec) + if err != nil { + return definitions.MuteTimeInterval{}, err + } + result := definitions.MuteTimeInterval{} + err = json.Unmarshal(b, &result) + if err != nil { + return definitions.MuteTimeInterval{}, err + } + result.Version = interval.ResourceVersion + err = result.Validate() + if err != nil { + return definitions.MuteTimeInterval{}, err + } + return result, nil +} diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go new file mode 100644 index 00000000000..64d95e17a12 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go @@ -0,0 +1,205 @@ +package timeinterval + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + notifications "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +var ( + _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) +) + +var resourceInfo = notifications.TimeIntervalResourceInfo + +type TimeIntervalService interface { + GetMuteTimings(ctx context.Context, orgID int64) ([]definitions.MuteTimeInterval, error) + GetMuteTiming(ctx context.Context, name string, orgID int64) (definitions.MuteTimeInterval, error) + CreateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (definitions.MuteTimeInterval, error) + UpdateMuteTiming(ctx context.Context, mt definitions.MuteTimeInterval, orgID int64) (definitions.MuteTimeInterval, error) + DeleteMuteTiming(ctx context.Context, name string, orgID int64, provenance definitions.Provenance, version string) error +} + +type legacyStorage struct { + service TimeIntervalService + namespacer request.NamespaceMapper + tableConverter rest.TableConvertor +} + +func (s *legacyStorage) New() runtime.Object { + return resourceInfo.NewFunc() +} + +func (s *legacyStorage) Destroy() {} + +func (s *legacyStorage) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *legacyStorage) GetSingularName() string { + return resourceInfo.GetSingularName() +} + +func (s *legacyStorage) NewList() runtime.Object { + return resourceInfo.NewListFunc() +} + +func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.tableConverter.ConvertToTable(ctx, object, tableOptions) +} + +func (s *legacyStorage) List(ctx context.Context, _ *internalversion.ListOptions) (runtime.Object, error) { + orgId, err := request.OrgIDForList(ctx) + if err != nil { + return nil, err + } + + res, err := s.service.GetMuteTimings(ctx, orgId) + if err != nil { + return nil, err + } + + return convertToK8sResources(orgId, res, s.namespacer) +} + +func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + timings, err := s.service.GetMuteTimings(ctx, info.OrgID) + if err != nil { + return nil, err + } + + for _, mt := range timings { + if getIntervalUID(mt) == uid { + return convertToK8sResource(info.OrgID, mt, s.namespacer) + } + } + return nil, errors.NewNotFound(resourceInfo.GroupResource(), uid) +} + +func (s *legacyStorage) Create(ctx context.Context, + obj runtime.Object, + createValidation rest.ValidateObjectFunc, + _ *metav1.CreateOptions, +) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + if createValidation != nil { + if err := createValidation(ctx, obj.DeepCopyObject()); err != nil { + return nil, err + } + } + p, ok := obj.(*notifications.TimeInterval) + if !ok { + return nil, fmt.Errorf("expected time-interval but got %s", obj.GetObjectKind().GroupVersionKind()) + } + if p.ObjectMeta.Name != "" { // TODO remove when metadata.name can be defined by user + return nil, errors.NewBadRequest("object's metadata.name should be empty") + } + model, err := convertToDomainModel(p) + if err != nil { + return nil, err + } + out, err := s.service.CreateMuteTiming(ctx, model, info.OrgID) + if err != nil { + return nil, err + } + return convertToK8sResource(info.OrgID, out, s.namespacer) +} + +func (s *legacyStorage) Update(ctx context.Context, + uid string, + objInfo rest.UpdatedObjectInfo, + createValidation rest.ValidateObjectFunc, + updateValidation rest.ValidateObjectUpdateFunc, + _ bool, + _ *metav1.UpdateOptions, +) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + old, err := s.Get(ctx, uid, nil) + if err != nil { + return old, false, err + } + obj, err := objInfo.UpdatedObject(ctx, old) + if err != nil { + return old, false, err + } + if updateValidation != nil { + if err := updateValidation(ctx, obj, old); err != nil { + return nil, false, err + } + } + p, ok := obj.(*notifications.TimeInterval) + if !ok { + return nil, false, fmt.Errorf("expected time-interval but got %s", obj.GetObjectKind().GroupVersionKind()) + } + interval, err := convertToDomainModel(p) + if err != nil { + return old, false, err + } + + if p.ObjectMeta.Name != getIntervalUID(interval) { + return nil, false, errors.NewBadRequest("title of cannot be changed. Consider creating a new resource.") + } + + updated, err := s.service.UpdateMuteTiming(ctx, interval, info.OrgID) + if err != nil { + return nil, false, err + } + + r, err := convertToK8sResource(info.OrgID, updated, s.namespacer) + return r, false, err +} + +// GracefulDeleter +func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + old, err := s.Get(ctx, uid, nil) + if err != nil { + return old, false, err + } + if deleteValidation != nil { + if err = deleteValidation(ctx, old); err != nil { + return nil, false, err + } + } + version := "" + if options.Preconditions != nil && options.Preconditions.ResourceVersion != nil { + version = *options.Preconditions.ResourceVersion + } + p, ok := old.(*notifications.TimeInterval) + if !ok { + return nil, false, fmt.Errorf("expected time-interval but got %s", old.GetObjectKind().GroupVersionKind()) + } + + err = s.service.DeleteMuteTiming(ctx, p.Spec.Name, info.OrgID, definitions.Provenance(models.ProvenanceNone), version) // TODO add support for dry-run option + return old, false, err // false - will be deleted async +} + +func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { + return nil, errors.NewMethodNotSupported(resourceInfo.GroupResource(), "deleteCollection") +} diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/storage.go b/pkg/registry/apis/alerting/notifications/timeinterval/storage.go new file mode 100644 index 00000000000..6b627c0c5e5 --- /dev/null +++ b/pkg/registry/apis/alerting/notifications/timeinterval/storage.go @@ -0,0 +1,79 @@ +package timeinterval + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/apiserver/pkg/registry/rest" + + "github.com/prometheus/client_golang/prometheus" + + model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/apiserver/utils" +) + +var _ grafanarest.Storage = (*storage)(nil) + +type storage struct { + *genericregistry.Store +} + +func (s storage) Compare(storageObj, legacyObj runtime.Object) bool { + // TODO implement when supported dual write mode is not Mode0 + return false +} + +func NewStorage( + legacySvc TimeIntervalService, + namespacer request.NamespaceMapper, + scheme *runtime.Scheme, + desiredMode grafanarest.DualWriterMode, + optsGetter generic.RESTOptionsGetter, + reg prometheus.Registerer) (rest.Storage, error) { + legacyStore := &legacyStorage{ + service: legacySvc, + namespacer: namespacer, + tableConverter: utils.NewTableConverter( + resourceInfo.GroupResource(), + []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + // {Name: "Intervals", Type: "string", Format: "string", Description: "The display name"}, + }, + func(obj any) ([]interface{}, error) { + r, ok := obj.(*model.TimeInterval) + if ok { + return []interface{}{ + r.Name, + // r.Spec, //TODO implement formatting for Spec, same as UI? + }, nil + } + return nil, fmt.Errorf("expected resource or info") + }), + } + if optsGetter != nil && desiredMode != grafanarest.Mode0 { + strategy := grafanaregistry.NewStrategy(scheme) + s := &genericregistry.Store{ + NewFunc: resourceInfo.NewFunc, + NewListFunc: resourceInfo.NewListFunc, + PredicateFunc: grafanaregistry.Matcher, + DefaultQualifiedResource: resourceInfo.GroupResource(), + SingularQualifiedResource: resourceInfo.SingularGroupResource(), + TableConvertor: legacyStore.tableConverter, + CreateStrategy: strategy, + UpdateStrategy: strategy, + DeleteStrategy: strategy, + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs} + if err := s.CompleteWithOptions(options); err != nil { + return nil, err + } + return grafanarest.NewDualWriter(desiredMode, legacyStore, storage{Store: s}, reg), nil + } + return legacyStore, nil +} diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 90b7de08755..3ccfae82528 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" @@ -35,6 +36,7 @@ func ProvideRegistryServiceSink( _ *peakq.PeakQAPIBuilder, _ *scope.ScopeAPIBuilder, _ *query.QueryAPIBuilder, + _ *notifications.NotificationsAPIBuilder, ) *Service { return &Service{} } diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index e2bd94e9e7c..d9782c44697 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -3,6 +3,7 @@ package apiregistry import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/registry/apis/alerting/notifications" "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" @@ -37,4 +38,5 @@ var WireSet = wire.NewSet( service.RegisterAPIService, query.RegisterAPIService, scope.RegisterAPIService, + notifications.RegisterAPIService, ) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 8076ef98fe6..7e68c0d8091 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -450,8 +450,9 @@ const ( ActionAlertingNotificationsWrite = "alert.notifications:write" // Alerting notifications time interval actions - ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read" - ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write" + ActionAlertingNotificationsTimeIntervalsRead = "alert.notifications.time-intervals:read" + ActionAlertingNotificationsTimeIntervalsWrite = "alert.notifications.time-intervals:write" + ActionAlertingNotificationsTimeIntervalsDelete = "alert.notifications.time-intervals:delete" // Alerting receiver actions ActionAlertingReceiversList = "alert.notifications.receivers:list" diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2c2cdbd1763..cc52ce2bcbe 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1355,6 +1355,13 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, + { + Name: "alertingApiServer", + Description: "Register Alerting APIs with the K8s API server", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + RequiresRestart: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d3552651a93..f286f4f5b22 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -179,3 +179,4 @@ failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false databaseReadReplica,experimental,@grafana/grafana-backend-services-squad,false,false,false zanzana,experimental,@grafana/identity-access-team,false,false,false passScopeToDashboardApi,experimental,@grafana/dashboards-squad,false,false,false +alertingApiServer,experimental,@grafana/alerting-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 2ddff1bb681..41267fbccdc 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -726,4 +726,8 @@ const ( // FlagPassScopeToDashboardApi // Enables the passing of scopes to dashboards fetching in Grafana FlagPassScopeToDashboardApi = "passScopeToDashboardApi" + + // FlagAlertingApiServer + // Register Alerting APIs with the K8s API server + FlagAlertingApiServer = "alertingApiServer" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 1535a47673d..a2338880416 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -90,6 +90,19 @@ "codeowner": "@grafana/alerting-squad" } }, + { + "metadata": { + "name": "alertingApiServer", + "resourceVersion": "1718908755156", + "creationTimestamp": "2024-06-20T18:39:15Z" + }, + "spec": { + "description": "Register Alerting APIs with the K8s API server", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true + } + }, { "metadata": { "name": "alertingBacktesting", @@ -2349,4 +2362,4 @@ } } ] -} +} \ No newline at end of file diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 952e1d86814..09819ac4e08 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -135,7 +135,7 @@ type AlertNG struct { stateManager *state.Manager folderService folder.Service dashboardService dashboards.DashboardService - api *api.API + Api *api.API // Alerting notification services MultiOrgAlertmanager *notifier.MultiOrgAlertmanager @@ -409,7 +409,7 @@ func (ng *AlertNG) init() error { ng.Cfg.UnifiedAlerting.RulesPerRuleGroupLimit, ng.Log, notifier.NewNotificationSettingsValidationService(ng.store), ac.NewRuleService(ng.accesscontrol)) - ng.api = &api.API{ + ng.Api = &api.API{ Cfg: ng.Cfg, DatasourceCache: ng.DataSourceCache, DatasourceService: ng.DataSourceService, @@ -438,7 +438,7 @@ func (ng *AlertNG) init() error { Hooks: api.NewHooks(ng.Log), Tracer: ng.tracer, } - ng.api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) + ng.Api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) if err := RegisterQuotas(ng.Cfg, ng.QuotaService, ng.store); err != nil { return err @@ -516,7 +516,7 @@ func (ng *AlertNG) IsDisabled() bool { // GetHooks returns a facility for replacing handlers for paths. The handler hook for a path // is invoked after all other middleware is invoked (authentication, instrumentation). func (ng *AlertNG) GetHooks() *api.Hooks { - return ng.api.Hooks + return ng.Api.Hooks } type Historian interface { diff --git a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go new file mode 100644 index 00000000000..c8141cbcdee --- /dev/null +++ b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go @@ -0,0 +1,541 @@ +package timeinterval + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/prometheus/alertmanager/config" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" + "github.com/grafana/grafana/pkg/generated/clientset/versioned" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "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/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" + "github.com/grafana/grafana/pkg/util" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func getTestHelper(t *testing.T) *apis.K8sTestHelper { + return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{ + featuremgmt.FlagAlertingApiServer, + }, + }) +} + +func TestIntegrationResourceIdentifier(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + client := adminK8sClient.NotificationsV0alpha1().TimeIntervals("default") + + newInterval := &v0alpha1.TimeInterval{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TimeIntervalSpec{ + Name: "time-newInterval", + TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2), + }, + } + + t.Run("create should fail if object name is specified", func(t *testing.T) { + interval := newInterval.DeepCopy() + interval.Name = "time-newInterval" + _, err := client.Create(ctx, interval, v1.CreateOptions{}) + require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err) + }) + + var resourceID string + t.Run("create should succeed and provide resource name", func(t *testing.T) { + actual, err := client.Create(ctx, newInterval, v1.CreateOptions{}) + require.NoError(t, err) + require.NotEmptyf(t, actual.Name, "Resource name should not be empty") + require.NotEmptyf(t, actual.UID, "Resource UID should not be empty") + resourceID = actual.Name + }) + + var existingInterval *v0alpha1.TimeInterval + t.Run("resource should be available by the identifier", func(t *testing.T) { + actual, err := client.Get(ctx, resourceID, v1.GetOptions{}) + require.NoError(t, err) + require.NotEmptyf(t, actual.Name, "Resource name should not be empty") + require.Equal(t, newInterval.Spec, actual.Spec) + existingInterval = actual + }) + + t.Run("update should fail if name in the specification changes", func(t *testing.T) { + if existingInterval == nil { + t.Skip() + } + updated := existingInterval.DeepCopy() + updated.Spec.Name = "another-newInterval" + _, err := client.Update(ctx, updated, v1.UpdateOptions{}) + require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err) + }) +} + +func TestIntegrationTimeIntervalAccessControl(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + org1 := helper.Org1 + + type testCase struct { + user apis.User + canRead bool + canUpdate bool + canCreate bool + canDelete bool + } + + reader := helper.CreateUser("IntervalsReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, + }, + }, + }) + writer := helper.CreateUser("IntervalsWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, + accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite, + }, + }, + }) + + deleter := helper.CreateUser("IntervalsDeleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{ + accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, + accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete, + }, + }, + }) + + testCases := []testCase{ + { + user: org1.Admin, + canRead: true, + canUpdate: true, + canCreate: true, + canDelete: true, + }, + { + user: org1.Editor, + canRead: true, + canUpdate: true, + canCreate: true, + canDelete: true, + }, + { + user: org1.Viewer, + canRead: true, + }, + { + user: reader, + canRead: true, + }, + { + user: writer, + canRead: true, + canCreate: true, + canUpdate: true, + }, + { + user: deleter, + canRead: true, + canDelete: true, + }, + } + + admin := org1.Admin + adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TimeIntervals("default") + + for _, tc := range testCases { + t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) { + k8sClient, err := versioned.NewForConfig(tc.user.NewRestConfig()) + require.NoError(t, err) + client := k8sClient.NotificationsV0alpha1().TimeIntervals("default") + + var expected = &v0alpha1.TimeInterval{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + Annotations: map[string]string{ + "grafana.com/provenance": "", + }, + }, + Spec: v0alpha1.TimeIntervalSpec{ + Name: fmt.Sprintf("time-interval-1-%s", tc.user.Identity.GetLogin()), + TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2), + }, + } + d, err := json.Marshal(expected) + require.NoError(t, err) + + if tc.canCreate { + t.Run("should be able to create time interval", func(t *testing.T) { + actual, err := client.Create(ctx, expected, v1.CreateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + require.Equal(t, expected.Spec, actual.Spec) + + t.Run("should fail if already exists", func(t *testing.T) { + _, err := client.Create(ctx, actual, v1.CreateOptions{}) + require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err) + }) + + expected = actual + }) + } else { + t.Run("should be forbidden to create", func(t *testing.T) { + _, err := client.Create(ctx, expected, v1.CreateOptions{}) + require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d)) + }) + + // create resource to proceed with other tests + expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + require.NotNil(t, expected) + } + + if tc.canRead { + t.Run("should be able to list time intervals", func(t *testing.T) { + list, err := client.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 1) + }) + + t.Run("should be able to read time interval by resource identifier", func(t *testing.T) { + got, err := client.Get(ctx, expected.Name, v1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, expected, got) + + t.Run("should get NotFound if resource does not exist", func(t *testing.T) { + _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to list time intervals", func(t *testing.T) { + _, err := client.List(ctx, v1.ListOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + + t.Run("should be forbidden to read time interval by name", func(t *testing.T) { + _, err := client.Get(ctx, expected.Name, v1.GetOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should get forbidden even if name does not exist", func(t *testing.T) { + _, err := client.Get(ctx, "Notfound", v1.GetOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + } + + updatedExpected := expected.DeepCopy() + updatedExpected.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2) + + d, err = json.Marshal(updatedExpected) + require.NoError(t, err) + + if tc.canUpdate { + t.Run("should be able to update time interval", func(t *testing.T) { + updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{}) + require.NoErrorf(t, err, "Payload %s", string(d)) + + expected = updated + + t.Run("should get NotFound if name does not exist", func(t *testing.T) { + up := updatedExpected.DeepCopy() + up.Name = "notFound" + _, err := client.Update(ctx, up, v1.UpdateOptions{}) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to update time interval", func(t *testing.T) { + _, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should get forbidden even if resource does not exist", func(t *testing.T) { + up := updatedExpected.DeepCopy() + up.Name = "notFound" + _, err := client.Update(ctx, up, v1.UpdateOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + } + + deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}} + + if tc.canDelete { + t.Run("should be able to delete time interval", func(t *testing.T) { + err := client.Delete(ctx, expected.Name, deleteOptions) + require.NoError(t, err) + + t.Run("should get NotFound if name does not exist", func(t *testing.T) { + err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) + require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err) + }) + }) + } else { + t.Run("should be forbidden to delete time interval", func(t *testing.T) { + err := client.Delete(ctx, expected.Name, deleteOptions) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + + t.Run("should be forbidden even if resource does not exist", func(t *testing.T) { + err := client.Delete(ctx, "notfound", v1.DeleteOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + }) + require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{})) + } + + if tc.canRead { + t.Run("should get empty list if no mute timings", func(t *testing.T) { + list, err := client.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Len(t, list.Items, 0) + }) + } + }) + } +} + +func TestIntegrationTimeIntervalProvisioning(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + org := helper.Org1 + + admin := org.Admin + adminK8sClient, err := versioned.NewForConfig(admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TimeIntervals("default") + + env := helper.GetEnv() + ac := acimpl.ProvideAccessControl(env.FeatureToggles) + db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac) + require.NoError(t, err) + + created, err := adminClient.Create(ctx, &v0alpha1.TimeInterval{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TimeIntervalSpec{ + Name: "time-interval-1", + TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2), + }, + }, v1.CreateOptions{}) + require.NoError(t, err) + require.Equal(t, "", created.Annotations["grafana.com/provenance"]) + + t.Run("should provide provenance status", func(t *testing.T) { + require.NoError(t, db.SetProvenance(ctx, &definitions.MuteTimeInterval{ + MuteTimeInterval: config.MuteTimeInterval{ + Name: created.Spec.Name, + }, + }, admin.Identity.GetOrgID(), "API")) + + got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, "API", got.Annotations["grafana.com/provenance"]) + }) + t.Run("should not let update if provisioned", func(t *testing.T) { + updated := created.DeepCopy() + updated.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2) + + _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) + + t.Run("should not let delete if provisioned", func(t *testing.T) { + err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{}) + require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + }) +} + +func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TimeIntervals("default") + + interval := v0alpha1.TimeInterval{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TimeIntervalSpec{ + Name: "time-interval", + TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2), + }, + } + + created, err := adminClient.Create(ctx, &interval, v1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, created) + require.NotEmpty(t, created.ResourceVersion) + + t.Run("should forbid if version does not match", func(t *testing.T) { + updated := created.DeepCopy() + updated.ResourceVersion = "test" + _, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) + }) + t.Run("should update if version matches", func(t *testing.T) { + updated := created.DeepCopy() + updated.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2) + actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.EqualValues(t, updated.Spec, actualUpdated.Spec) + require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion) + }) + t.Run("should update if version is empty", func(t *testing.T) { + updated := created.DeepCopy() + updated.ResourceVersion = "" + updated.Spec.TimeIntervals = v0alpha1.IntervalGenerator{}.GenerateMany(2) + + actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{}) + require.NoError(t, err) + require.EqualValues(t, updated.Spec, actualUpdated.Spec) + require.NotEqual(t, created.ResourceVersion, actualUpdated.ResourceVersion) + }) + t.Run("should fail to delete if version does not match", func(t *testing.T) { + actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) + require.NoError(t, err) + + err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ + Preconditions: &v1.Preconditions{ + ResourceVersion: util.Pointer("something"), + }, + }) + require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) + }) + t.Run("should succeed if version matches", func(t *testing.T) { + actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{}) + require.NoError(t, err) + + err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ + Preconditions: &v1.Preconditions{ + ResourceVersion: util.Pointer(actual.ResourceVersion), + }, + }) + require.NoError(t, err) + }) + t.Run("should succeed if version is empty", func(t *testing.T) { + actual, err := adminClient.Create(ctx, &interval, v1.CreateOptions{}) + require.NoError(t, err) + + err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{ + Preconditions: &v1.Preconditions{ + ResourceVersion: util.Pointer(actual.ResourceVersion), + }, + }) + require.NoError(t, err) + }) +} + +func TestIntegrationTimeIntervalPatch(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + helper := getTestHelper(t) + + adminK8sClient, err := versioned.NewForConfig(helper.Org1.Admin.NewRestConfig()) + require.NoError(t, err) + adminClient := adminK8sClient.NotificationsV0alpha1().TimeIntervals("default") + + interval := v0alpha1.TimeInterval{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "default", + }, + Spec: v0alpha1.TimeIntervalSpec{ + Name: "time-interval", + TimeIntervals: v0alpha1.IntervalGenerator{}.GenerateMany(2), + }, + } + + current, err := adminClient.Create(ctx, &interval, v1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, current) + require.NotEmpty(t, current.ResourceVersion) + + t.Run("should patch with merge patch", func(t *testing.T) { + patch := `{ + "spec": { + "time_intervals" : [] + } + }` + + result, err := adminClient.Patch(ctx, current.Name, types.MergePatchType, []byte(patch), v1.PatchOptions{}) + require.NoError(t, err) + require.Empty(t, result.Spec.TimeIntervals) + current = result + }) + + t.Run("should patch with json patch", func(t *testing.T) { + expected := v0alpha1.IntervalGenerator{}.Generate() + + patch := []map[string]interface{}{ + { + "op": "add", + "path": "/spec/time_intervals/-", + "value": expected, + }, + } + + patchData, err := json.Marshal(patch) + require.NoError(t, err) + + result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{}) + require.NoError(t, err) + expectedSpec := *current.Spec.DeepCopy() + expectedSpec.TimeIntervals = []v0alpha1.Interval{ + expected, + } + require.EqualValues(t, expectedSpec, result.Spec) + current = result + }) +} diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index c1e1529b2b6..5fa3c0633cb 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -28,8 +28,11 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" @@ -40,6 +43,8 @@ import ( "github.com/grafana/grafana/pkg/tests/testinfra" ) +const Org1 = "Org1" + type K8sTestHelper struct { t *testing.T env server.TestEnv @@ -63,7 +68,7 @@ func NewK8sTestHelper(t *testing.T, opts testinfra.GrafanaOpts) *K8sTestHelper { namespacer: request.GetNamespaceMapper(nil), } - c.Org1 = c.createTestUsers("Org1") + c.Org1 = c.createTestUsers(Org1) c.OrgB = c.createTestUsers("OrgB") c.loadAPIGroups() @@ -88,6 +93,10 @@ func (c *K8sTestHelper) loadAPIGroups() { } } +func (c *K8sTestHelper) GetEnv() server.TestEnv { + return c.env +} + func (c *K8sTestHelper) Shutdown() { err := c.env.Server.Shutdown(context.Background(), "done") require.NoError(c.t, err) @@ -172,7 +181,7 @@ func (c *K8sResourceClient) SanitizeJSON(v *unstructured.Unstructured) string { } out, err := json.MarshalIndent(copy, "", " ") - //fmt.Printf("%s", out) + // fmt.Printf("%s", out) require.NoError(c.t, err) return string(out) } @@ -374,7 +383,16 @@ func (c *K8sTestHelper) LoadYAMLOrJSON(body string) *unstructured.Unstructured { return &unstructured.Unstructured{Object: unstructuredMap} } -func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers { +func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers { + c.t.Helper() + return OrgUsers{ + Admin: c.CreateUser("admin", orgName, org.RoleAdmin, nil), + Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil), + Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil), + } +} + +func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.RoleType, permissions []resourcepermissions.SetResourcePermissionCommand) User { c.t.Helper() store := c.env.SQLStore @@ -389,9 +407,17 @@ func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers { require.NoError(c.t, err) orgId := int64(1) - if orgName != "Org1" { - orgId, err = orgService.GetOrCreate(context.Background(), orgName) - require.NoError(c.t, err) + if orgName != Org1 { + o, err := orgService.GetByName(context.Background(), &org.GetOrgByNameQuery{Name: orgName}) + if err != nil { + if !org.ErrOrgNotFound.Is(err) { + require.NoError(c.t, err) + } + orgId, err = orgService.GetOrCreate(context.Background(), orgName) + require.NoError(c.t, err) + } else { + orgId = o.ID + } } c.env.Cfg.AutoAssignOrg = true c.env.Cfg.AutoAssignOrgId = int(orgId) @@ -407,37 +433,52 @@ func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers { require.NoError(c.t, err) baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr()) - createUser := func(key string, role org.RoleType) User { - u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{ - DefaultOrgRole: string(role), - Password: user.Password(key), - Login: fmt.Sprintf("%s-%d", key, orgId), - OrgID: orgId, - }) - require.NoError(c.t, err) - require.Equal(c.t, orgId, u.OrgID) - require.True(c.t, u.ID > 0) - s, err := userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{ - UserID: u.ID, - Login: u.Login, - Email: u.Email, - OrgID: orgId, - }) - require.NoError(c.t, err) - require.Equal(c.t, orgId, s.OrgID) - require.Equal(c.t, role, s.OrgRole) // make sure the role was set properly + u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{ + DefaultOrgRole: string(basicRole), + Password: user.Password(name), + Login: fmt.Sprintf("%s-%d", name, orgId), + OrgID: orgId, + }) + require.NoError(c.t, err) + require.Equal(c.t, orgId, u.OrgID) + require.True(c.t, u.ID > 0) - return User{ - Identity: s, - password: key, - baseURL: baseUrl, - } + s, err := userSvc.GetSignedInUser(context.Background(), &user.GetSignedInUserQuery{ + UserID: u.ID, + Login: u.Login, + Email: u.Email, + OrgID: orgId, + }) + require.NoError(c.t, err) + require.Equal(c.t, orgId, s.OrgID) + require.Equal(c.t, basicRole, s.OrgRole) // make sure the role was set properly + + usr := User{ + Identity: s, + password: name, + baseURL: baseUrl, } - return OrgUsers{ - Admin: createUser("admin", org.RoleAdmin), - Editor: createUser("editor", org.RoleEditor), - Viewer: createUser("viewer", org.RoleViewer), + + if len(permissions) > 0 { + c.SetPermissions(usr, permissions) + } + + return usr +} + +func (c *K8sTestHelper) SetPermissions(user User, permissions []resourcepermissions.SetResourcePermissionCommand) { + id, err := user.Identity.GetID().UserID() + require.NoError(c.t, err) + + permissionsStore := resourcepermissions.NewStore(c.env.Cfg, c.env.SQLStore, featuremgmt.WithFeatures()) + + for _, permission := range permissions { + _, err := permissionsStore.SetUserResourcePermission(context.Background(), + user.Identity.GetOrgID(), + accesscontrol.User{ID: id}, + permission, nil) + require.NoError(c.t, err) } } From 491101bc8c5f7e8eb24145cce153a96a6069bf8c Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:20:49 -0600 Subject: [PATCH 068/122] Automation: Verify release artifacts on grafana.com (#89197) * baldm0mma/verify_release/ create verify_release_for_download function * baldm0mma/verify_release/ add name, image, env * baldm0mma/verify_release/ add initial commands * baldm0mma/verify_release/ add deps? * baldm0mma/verify_release/ update location * baldm0mma/verify_release/ add anno to lib-star * bald0mma/verify_release/ update func name to verify_grafanacom_step * baldm0mma/verify_release/ add verify shell script * baldm0mma/verify_release/ add script content, first attempt * baldm0mma/verify_release/ add node image to verify_grafanacom_step * baldm0mma/verify_release/ add gcom secret note * baldm0mma/verify_release/ add sudo to apt-get * baldm0mma/verify_release/ add anno * baldm0mma/verify_release/ add anno to secrets * baldm0mma/verify_release/ update commands to reflect node env image * baldm0mma/verify_release/ update annos * baldm0mma/verify_release/ update tag variable * baldm0mma/verify release/ add whitespace * baldm0mma/verify_releases/ update with no bash loops * baldm0mma/verify_release/ update exit logic * baldm0mma/verify_release/ remove annos * baldm0mma/verify_releasse/ resign and build yml * baldm0mma/verify_release/ remove annos * baldm0mma/verify_release/ update signature * baldm0mma/verify_release/ download curl * baldm0mma/verify_release/ remove temp key folder removal * baldm0mma/verify_release/ account for artifact download time * baldm0mma/verify_release/ add anno * baldm0mma/verify_release/ update location * baldm0mma/verify_release/ update script * baldm0mma/verify_release/ make drone * baldm0mma/verify_release/ update script for oss or ent * baldm0mma/verify_release/ add promotion option * baldm0mma/verify_release/ make drone * Update scripts/drone/events/release.star Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> * Update scripts/drone/steps/lib.star Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> * Update scripts/drone/steps/lib.star Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> * baldm0mma/verify_release/ update drone * Update scripts/drone/events/release.star Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> * baldm0mma/verify_release/ update drone * Update scripts/drone/steps/lib.star Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> * baldm0mma/update drone * baldm0mma/verify_release/ update path * baldm0mma/verify_release/ make drone * baldm0mma/update drone * Apply suggestions from code review Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> * baldm0mma/verify_release/ update for loop to account for failure * baldm0mma/verify_release/ make drone * baldm0mma/verify_release/ make format-drone * baldm0mma/verify_release/ rem unused var --------- Co-authored-by: Kevin Minehart <5140827+kminehart@users.noreply.github.com> --- .drone.yml | 46 ++++++++++++++++++++++++++- scripts/drone/events/release.star | 12 +++++++ scripts/drone/steps/lib.star | 28 ++++++++++++++++ scripts/drone/verify-grafanacom.sh | 51 ++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100755 scripts/drone/verify-grafanacom.sh diff --git a/.drone.yml b/.drone.yml index 58d8eaac4f8..0d3e0648a0f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2921,6 +2921,40 @@ volumes: path: /var/run/docker.sock name: docker --- +clone: + retries: 3 +depends_on: [] +image_pull_secrets: +- gcr +- gar +kind: pipeline +name: verify-grafanacom-artifacts +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - apk add curl bash + - "\n for i in {1..5}; do\n if ./scripts/drone/verify-grafanacom.sh; + then\n exit 0\n elif [ $i -eq 5 ]; then\n exit + 1\n else\n sleep 60\n fi\n done\n + \ " + depends_on: [] + image: node:20.9.0-alpine + name: verify-grafanacom +trigger: + event: + - promote + target: verify-grafanacom-artifacts +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- clone: retries: 3 depends_on: @@ -3001,6 +3035,16 @@ steps: from_secret: grafana_api_key image: grafana/grafana-ci-deploy:1.3.3 name: publish-grafanacom +- commands: + - apk add curl bash + - "\n for i in {1..5}; do\n if ./scripts/drone/verify-grafanacom.sh; + then\n exit 0\n elif [ $i -eq 5 ]; then\n exit + 1\n else\n sleep 60\n fi\n done\n + \ " + depends_on: + - publish-grafanacom + image: node:20.9.0-alpine + name: verify-grafanacom trigger: event: - promote @@ -4952,6 +4996,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: ba86e9c1fb16bb20bff8d56f158ea31f32c3e44f6d517a04ae774fc28f9101e7 +hmac: 06f574902baa67d8885abb48e48987f675d7637e30d4b783b3bb84e51b46cdaf ... diff --git a/scripts/drone/events/release.star b/scripts/drone/events/release.star index 404dafa79ca..00be52f614d 100644 --- a/scripts/drone/events/release.star +++ b/scripts/drone/events/release.star @@ -21,6 +21,7 @@ load( "remote_alertmanager_integration_tests_steps", "verify_gen_cue_step", "verify_gen_jsonnet_step", + "verify_grafanacom_step", "wire_install_step", "yarn_install_step", ) @@ -203,6 +204,7 @@ def publish_packages_pipeline(): publish_linux_packages_step(package_manager = "deb"), publish_linux_packages_step(package_manager = "rpm"), publish_grafanacom_step(ver_mode = "release"), + verify_grafanacom_step(), ] deps = [ @@ -211,6 +213,16 @@ def publish_packages_pipeline(): ] return [ + pipeline( + name = "verify-grafanacom-artifacts", + trigger = { + "event": ["promote"], + "target": "verify-grafanacom-artifacts", + }, + steps = [ + verify_grafanacom_step(depends_on = []), + ], + ), pipeline( name = "publish-packages", trigger = trigger, diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index bf5ae7a351f..e7694243679 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1152,6 +1152,34 @@ def publish_grafanacom_step(ver_mode): ], } +def verify_grafanacom_step(depends_on = ["publish-grafanacom"]): + return { + "name": "verify-grafanacom", + "image": images["node"], + "commands": [ + # Download and install `curl` and `bash` - both of which aren't available inside of the `node:{version}-alpine` docker image. + "apk add curl bash", + + # There may be a slight lag between when artifacts are uploaded to Google Storage, + # and when they become available on the website. This `for` loop sould account for that discrepancy. + # We attempt the verification up to 5 times. If successful, exit the loop with a success (0) status. + # If any attempt fails, but it's not the final attempt, wait 60 seconds before the next attempt. + # If the 5th (final) attempt fails, exit with error (1) status. + """ + for i in {1..5}; do + if ./scripts/drone/verify-grafanacom.sh; then + exit 0 + elif [ $i -eq 5 ]; then + exit 1 + else + sleep 60 + fi + done + """, + ], + "depends_on": depends_on, + } + def publish_linux_packages_step(package_manager = "deb"): return { "name": "publish-linux-packages-{}".format(package_manager), diff --git a/scripts/drone/verify-grafanacom.sh b/scripts/drone/verify-grafanacom.sh new file mode 100755 index 00000000000..b2950f8529c --- /dev/null +++ b/scripts/drone/verify-grafanacom.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +version=${1:-$TAG} + +# Construct the URL based on the provided version and edition +if [ "$EDITION" = "enterprise" ]; then + url="https://grafana.com/api/downloads/grafana-enterprise/versions/$version" +else + url="https://grafana.com/api/downloads/grafana/versions/$version" +fi + +# Make a request to the GCOM API to retrieve the artifacts for the specified version. Exit if the request fails. +if ! artifacts=$(curl "$url"); then + echo "Failed to retrieve artifact URLs from Grafana.com API. Please check the API key, authentication, edition, and version." + exit 1 +fi + +# Use Node.js to parse the JSON response and extract the download URLs +url_string=$(node -e " + const artifacts = JSON.parse(JSON.stringify($artifacts)); + const downloadUrls = artifacts.packages.map((package) => package.links.find((link) => link.rel === 'download').href); + console.log(downloadUrls.join(' ')); +") + +# Convert the url_string to a Bash array +read -r -a urls <<< "$url_string" + +# If empty, no artifact URLs were found for the specified version. Exit with an error. +if [ ${#urls[@]} -eq 0 ]; then + echo "No artifact URLs found for version $version. Please check the provided version." + exit 1 +fi + +# Iterate over the URLs and check the status code of each. If any URL does not return a 200 status code, add it to the failed_urls string. +failed_urls="" +for url in "${urls[@]}"; do + status_code=$(curl -L -s -o /dev/null -w "%{http_code}" "$url") + if [ "$status_code" -ne 200 ]; then + failed_urls+="$url\n" + fi +done + +# If any URLs failed, print them and exit with an error. +if [ -n "$failed_urls" ]; then + echo "The following URLs did not return a 200 status code:" + echo "$failed_urls" + exit 1 +else + echo "All URLs returned a 200 status code. Download links are valid for version $version." + exit 0 +fi From 61a102274d22e0f69442498c69eb5357d1eb0824 Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:13:51 -0400 Subject: [PATCH 069/122] datatrails: only store bookmark state instead of the entire trail step history (#86929) only store bookmark state instead of the entire step history --- public/app/features/trails/DataTrailCard.tsx | 53 ++-- public/app/features/trails/DataTrailsHome.tsx | 28 +- .../trails/TrailStore/TrailStore.test.ts | 244 +++++++++++++----- .../features/trails/TrailStore/TrailStore.ts | 105 ++++++-- public/app/features/trails/shared.ts | 3 +- 5 files changed, 321 insertions(+), 112 deletions(-) diff --git a/public/app/features/trails/DataTrailCard.tsx b/public/app/features/trails/DataTrailCard.tsx index a9ed32ee9b6..e441c5d3dc0 100644 --- a/public/app/features/trails/DataTrailCard.tsx +++ b/public/app/features/trails/DataTrailCard.tsx @@ -1,36 +1,57 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { useMemo } from 'react'; import { dateTimeFormat, GrafanaTheme2 } from '@grafana/data'; import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes'; import { Card, IconButton, Stack, Tag, useStyles2 } from '@grafana/ui'; import { DataTrail } from './DataTrail'; +import { getTrailStore, DataTrailBookmark } from './TrailStore/TrailStore'; import { VAR_FILTERS } from './shared'; import { getDataSource, getDataSourceName, getMetricName } from './utils'; -export interface Props { - trail: DataTrail; - onSelect: (trail: DataTrail) => void; +export type Props = { + trail?: DataTrail; + bookmark?: DataTrailBookmark; + onSelect: () => void; onDelete?: () => void; -} +}; -export function DataTrailCard({ trail, onSelect, onDelete }: Props) { +export function DataTrailCard(props: Props) { + const { onSelect, onDelete, bookmark } = props; const styles = useStyles2(getStyles); - const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, trail)!; - if (!(filtersVariable instanceof AdHocFiltersVariable)) { + const values = useMemo(() => { + let trail = props.trail || (bookmark && getTrailStore().getTrailForBookmark(bookmark)); + + if (!trail) { + return null; + } + + const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, trail)!; + if (!(filtersVariable instanceof AdHocFiltersVariable)) { + return null; + } + + const createdAt = bookmark?.createdAt || trail.state.createdAt; + + return { + dsValue: getDataSource(trail), + filters: filtersVariable.state.filters, + metric: trail.state.metric, + createdAt, + }; + }, [props.trail, bookmark]); + + if (!values) { return null; } - const filters = filtersVariable.state.filters; - const dsValue = getDataSource(trail); - - const onClick = () => onSelect(trail); + const { dsValue, filters, metric, createdAt } = values; return ( - - {getMetricName(trail.state.metric)} + + {getMetricName(metric)}
{filters.map((f) => ( @@ -43,9 +64,9 @@ export function DataTrailCard({ trail, onSelect, onDelete }: Props) {
Datasource: {getDataSourceName(dsValue)}
- {trail.state.createdAt && ( + {createdAt && ( - Created: {dateTimeFormat(trail.state.createdAt, { format: 'LL' })} + Created: {dateTimeFormat(createdAt, { format: 'LL' })} )}
diff --git a/public/app/features/trails/DataTrailsHome.tsx b/public/app/features/trails/DataTrailsHome.tsx index e65a65ff035..4e7a81144e0 100644 --- a/public/app/features/trails/DataTrailsHome.tsx +++ b/public/app/features/trails/DataTrailsHome.tsx @@ -10,7 +10,7 @@ import { Text } from '@grafana/ui/src/components/Text/Text'; import { DataTrail } from './DataTrail'; import { DataTrailCard } from './DataTrailCard'; import { DataTrailsApp } from './DataTrailsApp'; -import { getTrailStore } from './TrailStore/TrailStore'; +import { getBookmarkKey, getTrailStore } from './TrailStore/TrailStore'; import { reportExploreMetrics } from './interactions'; import { getDatasourceForNewTrail, getUrlForTrail, newMetricsTrail } from './utils'; @@ -29,9 +29,17 @@ export class DataTrailsHome extends SceneObjectBase { app.goToUrlForTrail(trail); }; - public onSelectTrail = (trail: DataTrail, isBookmark: boolean) => { + public onSelectRecentTrail = (trail: DataTrail) => { const app = getAppFor(this); - reportExploreMetrics('exploration_started', { cause: isBookmark ? 'bookmark_clicked' : 'recent_clicked' }); + reportExploreMetrics('exploration_started', { cause: 'recent_clicked' }); + getTrailStore().setRecentTrail(trail); + app.goToUrlForTrail(trail); + }; + + public onSelectBookmark = (bookmarkIndex: number) => { + const app = getAppFor(this); + reportExploreMetrics('exploration_started', { cause: 'bookmark_clicked' }); + const trail = getTrailStore().getTrailForBookmarkIndex(bookmarkIndex); getTrailStore().setRecentTrail(trail); app.goToUrlForTrail(trail); }; @@ -52,9 +60,6 @@ export class DataTrailsHome extends SceneObjectBase { return ; } - const onSelectRecent = (trail: DataTrail) => model.onSelectTrail(trail, false); - const onSelectBookmark = (trail: DataTrail) => model.onSelectTrail(trail, true); - return (
@@ -73,7 +78,7 @@ export class DataTrailsHome extends SceneObjectBase { model.onSelectRecentTrail(resolvedTrail)} /> ); })} @@ -83,13 +88,12 @@ export class DataTrailsHome extends SceneObjectBase {
Bookmarks
- {getTrailStore().bookmarks.map((trail, index) => { - const resolvedTrail = trail.resolve(); + {getTrailStore().bookmarks.map((bookmark, index) => { return ( model.onSelectBookmark(index)} onDelete={() => onDelete(index)} /> ); diff --git a/public/app/features/trails/TrailStore/TrailStore.test.ts b/public/app/features/trails/TrailStore/TrailStore.test.ts index 871dd2fc55c..d742f81cf9c 100644 --- a/public/app/features/trails/TrailStore/TrailStore.test.ts +++ b/public/app/features/trails/TrailStore/TrailStore.test.ts @@ -4,7 +4,7 @@ import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; import { MockDataSourceSrv, mockDataSource } from '../../alerting/unified/mocks'; import { DataTrail } from '../DataTrail'; -import { BOOKMARKED_TRAILS_KEY, RECENT_TRAILS_KEY, VAR_FILTERS } from '../shared'; +import { TRAIL_BOOKMARKS_KEY, RECENT_TRAILS_KEY, VAR_FILTERS } from '../shared'; import { SerializedTrail, getTrailStore } from './TrailStore'; @@ -482,11 +482,95 @@ describe('TrailStore', () => { }); }); - describe('Initialize store with one bookmark trail', () => { + describe('Initialize store with one bookmark trail but no recent trails', () => { beforeEach(() => { localStorage.clear(); localStorage.setItem( - BOOKMARKED_TRAILS_KEY, + TRAIL_BOOKMARKS_KEY, + JSON.stringify([ + { + urlValues: { + metric: 'bookmarked_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'time', + }, + ]) + ); + getTrailStore().load(); + }); + + const store = getTrailStore(); + + it('should have no recent trails', () => { + expect(store.recent.length).toBe(0); + }); + + it('should accurately load bookmarked trails xx', () => { + expect(store.bookmarks.length).toBe(1); + const trail = store.getTrailForBookmarkIndex(0); + expect(trail.state.metric).toBe('bookmarked_metric'); + }); + + it('should save a new recent trail based on the bookmark', () => { + expect(store.recent.length).toBe(0); + const trail = store.getTrailForBookmarkIndex(0); + // Trail and history must be activated first + trail.activate(); + trail.state.history.activate(); + store.setRecentTrail(trail); + expect(store.recent.length).toBe(1); + }); + + it('should be able to obtain index of bookmark', () => { + const trail = store.getTrailForBookmarkIndex(0); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(0); + }); + + it('index should be undefined for removed bookmarks', () => { + const trail = store.getTrailForBookmarkIndex(0); + store.removeBookmark(0); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(undefined); + }); + + it('index should be undefined for a trail that has changed since it was bookmarked', () => { + const trail = store.getTrailForBookmarkIndex(0); + trail.setState({ metric: 'something_completely_different' }); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(undefined); + }); + + it('should be able to obtain index of a bookmark for a trail that changed back to bookmarked state', () => { + const trail = store.getTrailForBookmarkIndex(0); + const bookmarkedMetric = trail.state.metric; + trail.setState({ metric: 'something_completely_different' }); + trail.setState({ metric: bookmarkedMetric }); + const index = store.getBookmarkIndex(trail); + expect(index).toBe(0); + }); + + it('should remove a bookmark', () => { + expect(store.bookmarks.length).toBe(1); + store.removeBookmark(0); + expect(store.bookmarks.length).toBe(0); + + jest.advanceTimersByTime(2000); + + expect(localStorage.getItem(TRAIL_BOOKMARKS_KEY)).toBe('[]'); + }); + }); + + describe('Initialize store with one legacy bookmark trail', () => { + beforeEach(() => { + localStorage.clear(); + localStorage.setItem( + TRAIL_BOOKMARKS_KEY, JSON.stringify([ { history: [ @@ -526,66 +610,18 @@ describe('TrailStore', () => { expect(store.recent.length).toBe(0); }); - it('should accurately load bookmarked trails', () => { + it('should accurately load legacy bookmark', () => { expect(store.bookmarks.length).toBe(1); - const trail = store.bookmarks[0].resolve(); - expect(trail.state.history.state.steps.length).toBe(2); - expect(trail.state.history.state.steps[0].type).toBe('start'); - expect(trail.state.history.state.steps[1].type).toBe('time'); - }); - - it('should save a new recent trail based on the bookmark', () => { - expect(store.recent.length).toBe(0); - const trail = store.bookmarks[0].resolve(); - store.setRecentTrail(trail); - expect(store.recent.length).toBe(1); - }); - - it('should be able to obtain index of bookmark', () => { - const trail = store.bookmarks[0].resolve(); - const index = store.getBookmarkIndex(trail); - expect(index).toBe(0); - }); - - it('index should be undefined for removed bookmarks', () => { - const trail = store.bookmarks[0].resolve(); - store.removeBookmark(0); - const index = store.getBookmarkIndex(trail); - expect(index).toBe(undefined); - }); - - it('index should be undefined for a trail that has changed since it was bookmarked', () => { - const trail = store.bookmarks[0].resolve(); - trail.setState({ metric: 'something_completely_different' }); - const index = store.getBookmarkIndex(trail); - expect(index).toBe(undefined); - }); - - it('should be able to obtain index of a bookmark for a trail that changed back to bookmarked state', () => { - const trail = store.bookmarks[0].resolve(); - const bookmarkedMetric = trail.state.metric; - trail.setState({ metric: 'something_completely_different' }); - trail.setState({ metric: bookmarkedMetric }); - const index = store.getBookmarkIndex(trail); - expect(index).toBe(0); - }); - - it('should remove a bookmark', () => { - expect(store.bookmarks.length).toBe(1); - store.removeBookmark(0); - expect(store.bookmarks.length).toBe(0); - - jest.advanceTimersByTime(2000); - - expect(localStorage.getItem(BOOKMARKED_TRAILS_KEY)).toBe('[]'); + const trail = store.getTrailForBookmarkIndex(0); + expect(trail.state.metric).toBe('access_permissions_duration_count'); }); }); - describe('Initialize store with one bookmark trail not on final step', () => { + describe('Initialize store with one legacy bookmark trail not bookmarked on final step', () => { beforeEach(() => { localStorage.clear(); localStorage.setItem( - BOOKMARKED_TRAILS_KEY, + TRAIL_BOOKMARKS_KEY, JSON.stringify([ { history: [ @@ -635,9 +671,87 @@ describe('TrailStore', () => { expect(store.recent.length).toBe(0); }); - it('should accurately load bookmarked trails', () => { + it('should accurately load legacy bookmark', () => { expect(store.bookmarks.length).toBe(1); - const trail = store.bookmarks[0].resolve(); + const trail = store.getTrailForBookmarkIndex(0); + expect(trail.state.metric).toBe('bookmarked_metric'); + }); + }); + + describe('Initialize store with one bookmark matching recent trail not on final step', () => { + beforeEach(() => { + localStorage.clear(); + localStorage.setItem( + RECENT_TRAILS_KEY, + JSON.stringify([ + { + history: [ + { + urlValues: { + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'start', + }, + { + urlValues: { + metric: 'bookmarked_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'time', + }, + { + urlValues: { + metric: 'some_other_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'metric', + }, + ], + currentStep: 1, + }, + ]) + ); + localStorage.setItem( + TRAIL_BOOKMARKS_KEY, + JSON.stringify([ + { + urlValues: { + metric: 'bookmarked_metric', + from: 'now-1h', + to: 'now', + 'var-ds': 'prom-mock', + 'var-filters': [], + refresh: '', + }, + type: 'time', + }, + ]) + ); + getTrailStore().load(); + }); + + const store = getTrailStore(); + + it('should have 1 recent trail', () => { + expect(store.recent.length).toBe(1); + }); + + it('should accurately load bookmarked trail from matching recent', () => { + expect(store.bookmarks.length).toBe(1); + expect(store.recent.length).toBe(1); + const trail = store.getTrailForBookmarkIndex(0); expect(trail.state.history.state.steps.length).toBe(3); expect(trail.state.history.state.steps[0].type).toBe('start'); expect(trail.state.history.state.steps[1].type).toBe('time'); @@ -645,34 +759,34 @@ describe('TrailStore', () => { }); it('should save a new recent trail based on the bookmark', () => { - expect(store.recent.length).toBe(0); - const trail = store.bookmarks[0].resolve(); + expect(store.recent.length).toBe(1); + const trail = store.getTrailForBookmarkIndex(0); store.setRecentTrail(trail); expect(store.recent.length).toBe(1); }); it('should be able to obtain index of bookmark', () => { - const trail = store.bookmarks[0].resolve(); + const trail = store.getTrailForBookmarkIndex(0); const index = store.getBookmarkIndex(trail); expect(index).toBe(0); }); it('index should be undefined for removed bookmarks', () => { - const trail = store.bookmarks[0].resolve(); + const trail = store.getTrailForBookmarkIndex(0); store.removeBookmark(0); const index = store.getBookmarkIndex(trail); expect(index).toBe(undefined); }); it('index should be undefined for a trail that has changed since it was bookmarked', () => { - const trail = store.bookmarks[0].resolve(); + const trail = store.getTrailForBookmarkIndex(0); trail.setState({ metric: 'something_completely_different' }); const index = store.getBookmarkIndex(trail); expect(index).toBe(undefined); }); it('should be able to obtain index of a bookmark for a trail that changed back to bookmarked state', () => { - const trail = store.bookmarks[0].resolve(); + const trail = store.getTrailForBookmarkIndex(0); trail.setState({ metric: 'something_completely_different' }); expect(store.getBookmarkIndex(trail)).toBe(undefined); trail.setState({ metric: 'bookmarked_metric' }); @@ -684,7 +798,7 @@ describe('TrailStore', () => { store.removeBookmark(0); expect(store.bookmarks.length).toBe(0); jest.advanceTimersByTime(2000); - expect(localStorage.getItem(BOOKMARKED_TRAILS_KEY)).toBe('[]'); + expect(localStorage.getItem(TRAIL_BOOKMARKS_KEY)).toBe('[]'); }); }); }); diff --git a/public/app/features/trails/TrailStore/TrailStore.ts b/public/app/features/trails/TrailStore/TrailStore.ts index 69e2ff119b6..e11c9c8abdb 100644 --- a/public/app/features/trails/TrailStore/TrailStore.ts +++ b/public/app/features/trails/TrailStore/TrailStore.ts @@ -7,7 +7,8 @@ import { dispatch } from 'app/store/store'; import { notifyApp } from '../../../core/reducers/appNotification'; import { DataTrail } from '../DataTrail'; import { TrailStepType } from '../DataTrailsHistory'; -import { BOOKMARKED_TRAILS_KEY, RECENT_TRAILS_KEY } from '../shared'; +import { TRAIL_BOOKMARKS_KEY, RECENT_TRAILS_KEY } from '../shared'; +import { newMetricsTrail } from '../utils'; import { createBookmarkSavedNotification } from './utils'; @@ -20,13 +21,18 @@ export interface SerializedTrail { description: string; parentIndex: number; }>; - currentStep: number; + currentStep?: number; // Assume last step in history if not specified createdAt?: number; } +export interface DataTrailBookmark { + urlValues: SceneObjectUrlValues; + createdAt: number; +} + export class TrailStore { private _recent: Array> = []; - private _bookmarks: Array> = []; + private _bookmarks: DataTrailBookmark[] = []; private _save: () => void; constructor() { @@ -38,8 +44,7 @@ export class TrailStore { .map((trail) => this._serializeTrail(trail.resolve())); localStorage.setItem(RECENT_TRAILS_KEY, JSON.stringify(serializedRecent)); - const serializedBookmarks = this._bookmarks.map((trail) => this._serializeTrail(trail.resolve())); - localStorage.setItem(BOOKMARKED_TRAILS_KEY, JSON.stringify(serializedBookmarks)); + localStorage.setItem(TRAIL_BOOKMARKS_KEY, JSON.stringify(this._bookmarks)); }; this._save = debounce(doSave, 1000); @@ -51,9 +56,9 @@ export class TrailStore { }); } - private _loadFromStorage(key: string) { + private _loadRecentTrailsFromStorage() { const list: Array> = []; - const storageItem = localStorage.getItem(key); + const storageItem = localStorage.getItem(RECENT_TRAILS_KEY); if (storageItem) { const serializedTrails: SerializedTrail[] = JSON.parse(storageItem); @@ -65,6 +70,25 @@ export class TrailStore { return list; } + private _loadBookmarksFromStorage() { + const storageItem = localStorage.getItem(TRAIL_BOOKMARKS_KEY); + + const list: Array = storageItem ? JSON.parse(storageItem) : []; + + return list.map((item) => { + if (isSerializedTrail(item)) { + // Take the legacy SerializedTrail implementation of bookmark storage, and extract a DataTrailBookmark + const step = item.currentStep != null ? item.currentStep : item.history.length - 1; + const bookmark: DataTrailBookmark = { + urlValues: item.history[step].urlValues, + createdAt: item.createdAt || Date.now(), + }; + return bookmark; + } + return item; + }); + } + private _deserializeTrail(t: SerializedTrail): DataTrail { // reconstruct the trail based on the serialized history const trail = new DataTrail({ createdAt: t.createdAt }); @@ -107,6 +131,31 @@ export class TrailStore { }; } + public getTrailForBookmarkIndex(index: number) { + const bookmark = this._bookmarks[index]; + if (!bookmark) { + // Create a blank trail + return newMetricsTrail(); + } + return this.getTrailForBookmark(bookmark); + } + + public getTrailForBookmark(bookmark: DataTrailBookmark) { + const key = getBookmarkKey(bookmark); + // Match for recent trails that have the exact same state as the current step + for (const recent of this._recent) { + const trail = recent.resolve(); + if (getBookmarkKey(trail) === key) { + return trail; + } + } + // Just create a new trail with that state + + const trail = new DataTrail({}); + this._loadFromUrl(trail, bookmark.urlValues); + return trail; + } + private _loadFromUrl(node: SceneObject, urlValues: SceneObjectUrlValues) { const urlState = urlUtil.renderUrl('', urlValues); sceneUtils.syncStateFromSearchParams(node, new URLSearchParams(urlState)); @@ -118,8 +167,8 @@ export class TrailStore { } load() { - this._recent = this._loadFromStorage(RECENT_TRAILS_KEY); - this._bookmarks = this._loadFromStorage(BOOKMARKED_TRAILS_KEY); + this._recent = this._loadRecentTrailsFromStorage(); + this._bookmarks = this._loadBookmarksFromStorage(); this._refreshBookmarkIndexMap(); } @@ -152,8 +201,14 @@ export class TrailStore { } addBookmark(trail: DataTrail) { - const bookmark = new DataTrail(sceneUtils.cloneSceneObjectState(trail.state)); - this._bookmarks.unshift(bookmark.getRef()); + const urlState = getUrlSyncManager().getUrlState(trail); + + const bookmarkState: DataTrailBookmark = { + urlValues: urlState, + createdAt: Date.now(), + }; + + this._bookmarks.unshift(bookmarkState); this._refreshBookmarkIndexMap(); this._save(); dispatch(notifyApp(createBookmarkSavedNotification())); @@ -178,9 +233,7 @@ export class TrailStore { private _refreshBookmarkIndexMap() { this._bookmarkIndexMap.clear(); this._bookmarks.forEach((bookmarked, index) => { - const trail = bookmarked.resolve(); - - const key = getBookmarkKey(trail); + const key = getBookmarkKey(bookmarked); // If there are duplicate bookmarks, the latest index will be kept this._bookmarkIndexMap.set(key, index); }); @@ -190,24 +243,36 @@ export class TrailStore { function getUrlStateForComparison(trail: DataTrail) { const urlState = getUrlSyncManager().getUrlState(trail); // Make a few corrections + correctUrlStateForComparison(urlState); + return urlState; +} + +function correctUrlStateForComparison(urlState: SceneObjectUrlValues) { // Omit some URL parameters that are not useful for state comparison, // as they can change in the URL without creating new steps delete urlState.actionView; delete urlState.layout; delete urlState.metricSearch; + delete urlState.refresh; // Populate defaults - if (urlState['var-groupby'] === '') { + if (urlState['var-groupby'] === '' || urlState['var-groupby'] === undefined) { urlState['var-groupby'] = '$__all'; } + if (typeof urlState['var-filters'] !== 'string') { + urlState['var-filters'] = urlState['var-filters']?.filter((filter) => filter !== ''); + } + return urlState; } -function getBookmarkKey(trail: DataTrail) { - const key = JSON.stringify(getUrlStateForComparison(trail)); - return key; +export function getBookmarkKey(trail: DataTrail | DataTrailBookmark) { + if (trail instanceof DataTrail) { + return JSON.stringify(getUrlStateForComparison(trail)); + } + return JSON.stringify(correctUrlStateForComparison({ ...trail.urlValues })); } let store: TrailStore | undefined; @@ -218,3 +283,7 @@ export function getTrailStore(): TrailStore { return store; } + +function isSerializedTrail(serialized: unknown): serialized is SerializedTrail { + return serialized != null && typeof serialized === 'object' && 'history' in serialized; +} diff --git a/public/app/features/trails/shared.ts b/public/app/features/trails/shared.ts index 7c8ed5697b3..4b12725cb9a 100644 --- a/public/app/features/trails/shared.ts +++ b/public/app/features/trails/shared.ts @@ -31,7 +31,8 @@ export const trailDS = { uid: VAR_DATASOURCE_EXPR }; // Local storage keys export const RECENT_TRAILS_KEY = 'grafana.trails.recent'; -export const BOOKMARKED_TRAILS_KEY = 'grafana.trails.bookmarks'; + +export const TRAIL_BOOKMARKS_KEY = 'grafana.trails.bookmarks'; export type MakeOptional = Pick, K> & Omit; From f74afa5e7f40a30222cfe31cced34d924598d643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Tolnai?= Date: Fri, 21 Jun 2024 08:54:12 +0200 Subject: [PATCH 070/122] Codeowners: Remove `@mikkancso` from codeowners (#89471) remove mikkancso from codeowners --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf7bc7537ad..e26d5baa7a2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -409,12 +409,12 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/geo/ @grafana/dataviz-squad /public/app/features/visualization/data-hover/ @grafana/dataviz-squad /public/app/features/commandPalette/ @grafana/grafana-frontend-platform -/public/app/features/connections/ @grafana/plugins-platform-frontend @mikkancso +/public/app/features/connections/ @grafana/plugins-platform-frontend /public/app/features/correlations/ @grafana/explore-squad /public/app/features/dashboard/ @grafana/dashboards-squad /public/app/features/dashboard/components/TransformationsEditor/ @grafana/dataviz-squad /public/app/features/dashboard-scene/ @grafana/dashboards-squad -/public/app/features/datasources/ @grafana/plugins-platform-frontend @mikkancso +/public/app/features/datasources/ @grafana/plugins-platform-frontend /public/app/features/dimensions/ @grafana/dataviz-squad /public/app/features/dataframe-import/ @grafana/dataviz-squad /public/app/features/explore/ @grafana/explore-squad From 93ef90a1e411d6b4372f3d5e8a175525c8fcb466 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 21 Jun 2024 10:23:08 +0300 Subject: [PATCH 071/122] K8s: remove (not great) example api (#89524) --- pkg/apis/example/v0alpha1/doc.go | 11 - pkg/apis/example/v0alpha1/register.go | 30 --- pkg/apis/example/v0alpha1/types.go | 48 ---- .../example/v0alpha1/zz_generated.deepcopy.go | 122 ---------- .../example/v0alpha1/zz_generated.defaults.go | 19 -- .../example/v0alpha1/zz_generated.openapi.go | 219 ----------------- ...enerated.openapi_violation_exceptions.list | 1 - pkg/registry/apis/apis.go | 2 - pkg/registry/apis/example/dummy_storage.go | 128 ---------- pkg/registry/apis/example/register.go | 226 ------------------ pkg/registry/apis/example/storage.go | 86 ------- pkg/registry/apis/example/subresource.go | 54 ----- pkg/registry/apis/wireset.go | 2 - pkg/services/apiserver/standalone/factory.go | 4 - pkg/tests/apis/example/example_test.go | 176 -------------- 15 files changed, 1128 deletions(-) delete mode 100644 pkg/apis/example/v0alpha1/doc.go delete mode 100644 pkg/apis/example/v0alpha1/register.go delete mode 100644 pkg/apis/example/v0alpha1/types.go delete mode 100644 pkg/apis/example/v0alpha1/zz_generated.deepcopy.go delete mode 100644 pkg/apis/example/v0alpha1/zz_generated.defaults.go delete mode 100644 pkg/apis/example/v0alpha1/zz_generated.openapi.go delete mode 100644 pkg/apis/example/v0alpha1/zz_generated.openapi_violation_exceptions.list delete mode 100644 pkg/registry/apis/example/dummy_storage.go delete mode 100644 pkg/registry/apis/example/register.go delete mode 100644 pkg/registry/apis/example/storage.go delete mode 100644 pkg/registry/apis/example/subresource.go delete mode 100644 pkg/tests/apis/example/example_test.go diff --git a/pkg/apis/example/v0alpha1/doc.go b/pkg/apis/example/v0alpha1/doc.go deleted file mode 100644 index 96849073b83..00000000000 --- a/pkg/apis/example/v0alpha1/doc.go +++ /dev/null @@ -1,11 +0,0 @@ -// +k8s:deepcopy-gen=package -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta -// +groupName=example.grafana.app - -// The testing api is a dependency free service that we can use to experiment with -// api aggregation across multiple deployment models. Specifically: -// - standalone: running as part of the standard grafana build -// - aggregated: running as the target - -package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/example/v0alpha1" diff --git a/pkg/apis/example/v0alpha1/register.go b/pkg/apis/example/v0alpha1/register.go deleted file mode 100644 index e254cad1d34..00000000000 --- a/pkg/apis/example/v0alpha1/register.go +++ /dev/null @@ -1,30 +0,0 @@ -package v0alpha1 - -import ( - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" -) - -const ( - GROUP = "example.grafana.app" - VERSION = "v0alpha1" - APIVERSION = GROUP + "/" + VERSION -) - -var RuntimeResourceInfo = common.NewResourceInfo(GROUP, VERSION, - "runtime", "runtime", "RuntimeInfo", - func() runtime.Object { return &RuntimeInfo{} }, - func() runtime.Object { return &RuntimeInfo{} }, -) -var DummyResourceInfo = common.NewResourceInfo(GROUP, VERSION, - "dummy", "dummy", "DummyResource", - func() runtime.Object { return &DummyResource{} }, - func() runtime.Object { return &DummyResourceList{} }, -) - -var ( - // SchemeGroupVersion is group version used to register these objects - SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} -) diff --git a/pkg/apis/example/v0alpha1/types.go b/pkg/apis/example/v0alpha1/types.go deleted file mode 100644 index a8fef3ffe6f..00000000000 --- a/pkg/apis/example/v0alpha1/types.go +++ /dev/null @@ -1,48 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" -) - -// Mirrors the info exposed in "github.com/grafana/grafana/pkg/setting" -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type RuntimeInfo struct { - metav1.TypeMeta `json:",inline"` - - // Unix timestamp when the process started - StartupTime int64 `json:"startupTime,omitempty"` - - BuildVersion string `json:"buildVersion,omitempty"` - BuildCommit string `json:"buildCommit,omitempty"` - EnterpriseBuildCommit string `json:"enterpriseBuildCommit,omitempty"` - BuildBranch string `json:"buildBranch,omitempty"` - BuildStamp int64 `json:"buildStamp,omitempty"` - IsEnterprise bool `json:"enterprise,omitempty"` - Packaging string `json:"packaging,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DummyResource struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec common.Unstructured `json:"spec,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DummyResourceList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []DummyResource `json:"items,omitempty"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DummySubresource struct { - metav1.TypeMeta `json:",inline"` - - // add subresource info here - Info string `json:"info,omitempty"` -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/example/v0alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 063ad395718..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,122 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DummyResource) DeepCopyInto(out *DummyResource) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DummyResource. -func (in *DummyResource) DeepCopy() *DummyResource { - if in == nil { - return nil - } - out := new(DummyResource) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DummyResource) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DummyResourceList) DeepCopyInto(out *DummyResourceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DummyResource, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DummyResourceList. -func (in *DummyResourceList) DeepCopy() *DummyResourceList { - if in == nil { - return nil - } - out := new(DummyResourceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DummyResourceList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DummySubresource) DeepCopyInto(out *DummySubresource) { - *out = *in - out.TypeMeta = in.TypeMeta - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DummySubresource. -func (in *DummySubresource) DeepCopy() *DummySubresource { - if in == nil { - return nil - } - out := new(DummySubresource) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DummySubresource) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RuntimeInfo) DeepCopyInto(out *RuntimeInfo) { - *out = *in - out.TypeMeta = in.TypeMeta - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuntimeInfo. -func (in *RuntimeInfo) DeepCopy() *RuntimeInfo { - if in == nil { - return nil - } - out := new(RuntimeInfo) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *RuntimeInfo) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.defaults.go b/pkg/apis/example/v0alpha1/zz_generated.defaults.go deleted file mode 100644 index 238fc2f4edc..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.defaults.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by defaulter-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterDefaults adds defaulters functions to the given scheme. -// Public to allow building arbitrary schemes. -// All generated defaulters are covering - they call all nested defaulters. -func RegisterDefaults(scheme *runtime.Scheme) error { - return nil -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.openapi.go b/pkg/apis/example/v0alpha1/zz_generated.openapi.go deleted file mode 100644 index 8c441c55064..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.openapi.go +++ /dev/null @@ -1,219 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by openapi-gen. DO NOT EDIT. - -// This file was autogenerated by openapi-gen. Do not edit it manually! - -package v0alpha1 - -import ( - common "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResource": schema_pkg_apis_example_v0alpha1_DummyResource(ref), - "github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResourceList": schema_pkg_apis_example_v0alpha1_DummyResourceList(ref), - "github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummySubresource": schema_pkg_apis_example_v0alpha1_DummySubresource(ref), - "github.com/grafana/grafana/pkg/apis/example/v0alpha1.RuntimeInfo": schema_pkg_apis_example_v0alpha1_RuntimeInfo(ref), - } -} - -func schema_pkg_apis_example_v0alpha1_DummyResource(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_example_v0alpha1_DummyResourceList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResource"), - }, - }, - }, - }, - }, - }, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/example/v0alpha1.DummyResource", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_example_v0alpha1_DummySubresource(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "info": { - SchemaProps: spec.SchemaProps{ - Description: "add subresource info here", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - } -} - -func schema_pkg_apis_example_v0alpha1_RuntimeInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Mirrors the info exposed in \"github.com/grafana/grafana/pkg/setting\"", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "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", - Type: []string{"string"}, - Format: "", - }, - }, - "startupTime": { - SchemaProps: spec.SchemaProps{ - Description: "Unix timestamp when the process started", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "buildVersion": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "buildCommit": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "enterpriseBuildCommit": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "buildBranch": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "buildStamp": { - SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", - }, - }, - "enterprise": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - "packaging": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - } -} diff --git a/pkg/apis/example/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/example/v0alpha1/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index 75c16b7b44e..00000000000 --- a/pkg/apis/example/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1 +0,0 @@ -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/example/v0alpha1,RuntimeInfo,IsEnterprise diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 3ccfae82528..a44271582af 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" - "github.com/grafana/grafana/pkg/registry/apis/example" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/peakq" @@ -28,7 +27,6 @@ type Service struct{} func ProvideRegistryServiceSink( _ *dashboard.DashboardsAPIBuilder, _ *playlist.PlaylistAPIBuilder, - _ *example.TestingAPIBuilder, _ *dashboardsnapshot.SnapshotsAPIBuilder, _ *featuretoggle.FeatureFlagAPIBuilder, _ *datasource.DataSourceAPIBuilder, diff --git a/pkg/registry/apis/example/dummy_storage.go b/pkg/registry/apis/example/dummy_storage.go deleted file mode 100644 index 574a87dd527..00000000000 --- a/pkg/registry/apis/example/dummy_storage.go +++ /dev/null @@ -1,128 +0,0 @@ -package example - -import ( - "context" - "fmt" - "slices" - - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "k8s.io/apiserver/pkg/registry/rest" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - grafanarequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" -) - -var ( - _ rest.Storage = (*dummyStorage)(nil) - _ rest.Scoper = (*dummyStorage)(nil) - _ rest.SingularNameProvider = (*dummyStorage)(nil) - _ rest.Getter = (*dummyStorage)(nil) - _ rest.Lister = (*dummyStorage)(nil) -) - -type dummyStorage struct { - store *genericregistry.Store - names []string - creationTimestamp metav1.Time -} - -func newDummyStorage(gv schema.GroupVersion, scheme *runtime.Scheme, names ...string) *dummyStorage { - var resourceInfo = example.DummyResourceInfo - strategy := grafanaregistry.NewStrategy(scheme) - store := &genericregistry.Store{ - NewFunc: resourceInfo.NewFunc, - NewListFunc: resourceInfo.NewListFunc, - PredicateFunc: grafanaregistry.Matcher, - DefaultQualifiedResource: resourceInfo.GroupResource(), - SingularQualifiedResource: resourceInfo.SingularGroupResource(), - TableConvertor: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()), - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - } - - return &dummyStorage{ - store: store, - names: names, - creationTimestamp: metav1.Now(), - } -} - -func (s *dummyStorage) New() runtime.Object { - return s.store.New() -} - -func (s *dummyStorage) Destroy() {} - -func (s *dummyStorage) NamespaceScoped() bool { - return true -} - -func (s *dummyStorage) GetSingularName() string { - return example.DummyResourceInfo.GetSingularName() -} - -func (s *dummyStorage) NewList() runtime.Object { - return s.store.NewListFunc() -} - -func (s *dummyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.store.TableConvertor.ConvertToTable(ctx, object, tableOptions) -} - -func (s *dummyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - info, err := grafanarequest.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - idx := slices.Index(s.names, name) - if idx < 0 { - return nil, fmt.Errorf("dummy not found") - } - - return &example.DummyResource{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: info.Value, - CreationTimestamp: s.creationTimestamp, - ResourceVersion: "1", - }, - Spec: common.Unstructured{ - Object: map[string]any{ - "Dummy": name, - }, - }, - }, nil -} - -func (s *dummyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - info, err := grafanarequest.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - res := &example.DummyResourceList{} - for _, name := range s.names { - res.Items = append(res.Items, example.DummyResource{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: info.Value, - CreationTimestamp: s.creationTimestamp, - ResourceVersion: "1", - }, - Spec: common.Unstructured{ - Object: map[string]any{ - "Dummy": name, - }, - }, - }) - } - return res, nil -} diff --git a/pkg/registry/apis/example/register.go b/pkg/registry/apis/example/register.go deleted file mode 100644 index 6dc5644bffa..00000000000 --- a/pkg/registry/apis/example/register.go +++ /dev/null @@ -1,226 +0,0 @@ -package example - -import ( - "context" - "fmt" - "net/http" - - "github.com/prometheus/client_golang/prometheus" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" - "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/apiserver/pkg/registry/generic" - "k8s.io/apiserver/pkg/registry/rest" - genericapiserver "k8s.io/apiserver/pkg/server" - common "k8s.io/kube-openapi/pkg/common" - "k8s.io/kube-openapi/pkg/spec3" - "k8s.io/kube-openapi/pkg/validation/spec" - - "github.com/grafana/grafana/pkg/apimachinery/identity" - example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - "github.com/grafana/grafana/pkg/apiserver/builder" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" - "github.com/grafana/grafana/pkg/services/featuremgmt" -) - -var _ builder.APIGroupBuilder = (*TestingAPIBuilder)(nil) - -// This is used just so wire has something unique to return -type TestingAPIBuilder struct { - codecs serializer.CodecFactory - gv schema.GroupVersion -} - -func NewTestingAPIBuilder() *TestingAPIBuilder { - return &TestingAPIBuilder{ - gv: schema.GroupVersion{Group: example.GROUP, Version: example.VERSION}, - } -} - -func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration builder.APIRegistrar, reg prometheus.Registerer) *TestingAPIBuilder { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil // skip registration unless opting into experimental apis - } - builder := NewTestingAPIBuilder() - apiregistration.RegisterAPI(builder) - return builder -} - -func (b *TestingAPIBuilder) GetGroupVersion() schema.GroupVersion { - return b.gv -} - -func (b *TestingAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode { - // Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go - return grafanarest.Mode0 -} - -func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { - scheme.AddKnownTypes(gv, - &example.RuntimeInfo{}, - &example.DummyResource{}, - &example.DummyResourceList{}, - &example.DummySubresource{}, - ) -} - -func (b *TestingAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - addKnownTypes(scheme, b.gv) - - // Link this version to the internal representation. - // This is used for server-side-apply (PATCH), and avoids the error: - // "no kind is registered for the type" - addKnownTypes(scheme, schema.GroupVersion{ - Group: b.gv.Group, - Version: runtime.APIVersionInternal, - }) - - // If multiple versions exist, then register conversions from zz_generated.conversion.go - // if err := playlist.RegisterConversions(scheme); err != nil { - // return err - // } - metav1.AddToGroupVersion(scheme, b.gv) - return scheme.SetVersionPriority(b.gv) -} - -func (b *TestingAPIBuilder) GetAPIGroupInfo( - scheme *runtime.Scheme, - codecs serializer.CodecFactory, // pointer? - _ generic.RESTOptionsGetter, - _ grafanarest.DualWriterMode, - _ prometheus.Registerer, -) (*genericapiserver.APIGroupInfo, error) { - b.codecs = codecs - apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(b.gv.Group, scheme, metav1.ParameterCodec, codecs) - - storage := map[string]rest.Storage{} - storage[example.RuntimeResourceInfo.StoragePath()] = newDeploymentInfoStorage(b.gv, scheme) - storage[example.DummyResourceInfo.StoragePath()] = newDummyStorage(b.gv, scheme, "test1", "test2", "test3") - storage[example.DummyResourceInfo.StoragePath("sub")] = &dummySubresourceREST{} - apiGroupInfo.VersionedResourcesStorageMap[b.gv.Version] = storage - return &apiGroupInfo, nil -} - -func (b *TestingAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return example.GetOpenAPIDefinitions -} - -// Register additional routes with the server -func (b *TestingAPIBuilder) GetAPIRoutes() *builder.APIRoutes { - return &builder.APIRoutes{ - Root: []builder.APIRouteHandler{ - { - Path: "aaa", - Spec: &spec3.PathProps{ - Summary: "an example at the root level", - Description: "longer description here?", - Get: &spec3.Operation{ - OperationProps: spec3.OperationProps{ - Parameters: []*spec3.Parameter{ - {ParameterProps: spec3.ParameterProps{ - Name: "a", - }}, - }, - Responses: &spec3.Responses{ - ResponsesProps: spec3.ResponsesProps{ - StatusCodeResponses: map[int]*spec3.Response{ - 200: { - ResponseProps: spec3.ResponseProps{ - Description: "OK", - Content: map[string]*spec3.MediaType{ - "text/plain": { - MediaTypeProps: spec3.MediaTypeProps{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - Handler: func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("Root level handler (aaa)")) - }, - }, - { - Path: "bbb", - Spec: &spec3.PathProps{ - Summary: "an example at the root level", - Description: "longer description here?", - Get: &spec3.Operation{ - OperationProps: spec3.OperationProps{ - Parameters: []*spec3.Parameter{ - {ParameterProps: spec3.ParameterProps{ - Name: "b", - }}, - }, - }, - }, - }, - Handler: func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("Root level handler (bbb)")) - }, - }, - }, - Namespace: []builder.APIRouteHandler{ - { - Path: "ccc", - Spec: &spec3.PathProps{ - Summary: "an example at the root level", - Description: "longer description here?", - Get: &spec3.Operation{ - OperationProps: spec3.OperationProps{ - Parameters: []*spec3.Parameter{ - {ParameterProps: spec3.ParameterProps{ - Name: "a", - }}, - }, - }, - }, - }, - Handler: func(w http.ResponseWriter, r *http.Request) { - info, ok := request.RequestInfoFrom(r.Context()) - if !ok { - responsewriters.ErrorNegotiated( - apierrors.NewInternalError(fmt.Errorf("no RequestInfo found in the context")), - b.codecs, schema.GroupVersion{}, w, r, - ) - return - } - - _, _ = w.Write([]byte("Custom namespace route ccc: " + info.Namespace)) - }, - }, - }, - } -} - -func (b *TestingAPIBuilder) GetAuthorizer() authorizer.Authorizer { - return authorizer.AuthorizerFunc( - func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - if !attr.IsResourceRequest() { - return authorizer.DecisionNoOpinion, "", nil - } - - // require a user - _, err = identity.GetRequester(ctx) - if err != nil { - return authorizer.DecisionDeny, "valid user is required", err - } - - return authorizer.DecisionNoOpinion, "", err // fallback to org/role logic - }) -} diff --git a/pkg/registry/apis/example/storage.go b/pkg/registry/apis/example/storage.go deleted file mode 100644 index 4e2c1858837..00000000000 --- a/pkg/registry/apis/example/storage.go +++ /dev/null @@ -1,86 +0,0 @@ -package example - -import ( - "context" - "time" - - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" - "k8s.io/apiserver/pkg/registry/rest" - - example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - "github.com/grafana/grafana/pkg/setting" -) - -var ( - _ rest.Storage = (*staticStorage)(nil) - _ rest.Scoper = (*staticStorage)(nil) - _ rest.SingularNameProvider = (*staticStorage)(nil) - _ rest.Lister = (*staticStorage)(nil) -) - -type staticStorage struct { - Store *genericregistry.Store - info example.RuntimeInfo -} - -func newDeploymentInfoStorage(gv schema.GroupVersion, scheme *runtime.Scheme) *staticStorage { - var resourceInfo = example.RuntimeResourceInfo - strategy := grafanaregistry.NewStrategy(scheme) - store := &genericregistry.Store{ - NewFunc: resourceInfo.NewFunc, - NewListFunc: resourceInfo.NewListFunc, - PredicateFunc: grafanaregistry.Matcher, - DefaultQualifiedResource: resourceInfo.GroupResource(), - SingularQualifiedResource: resourceInfo.SingularGroupResource(), - TableConvertor: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()), - CreateStrategy: strategy, - UpdateStrategy: strategy, - DeleteStrategy: strategy, - } - - return &staticStorage{ - Store: store, - info: example.RuntimeInfo{ - TypeMeta: example.RuntimeResourceInfo.TypeMeta(), - BuildVersion: setting.BuildVersion, - BuildCommit: setting.BuildCommit, - BuildBranch: setting.BuildBranch, - EnterpriseBuildCommit: setting.EnterpriseBuildCommit, - BuildStamp: setting.BuildStamp, - IsEnterprise: setting.IsEnterprise, - Packaging: setting.Packaging, - StartupTime: time.Now().UnixMilli(), - }, - } -} - -func (s *staticStorage) New() runtime.Object { - return s.Store.New() -} - -func (s *staticStorage) Destroy() {} - -func (s *staticStorage) NamespaceScoped() bool { - return false -} - -func (s *staticStorage) GetSingularName() string { - return example.RuntimeResourceInfo.GetSingularName() -} - -func (s *staticStorage) NewList() runtime.Object { - return s.Store.NewListFunc() -} - -func (s *staticStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.Store.TableConvertor.ConvertToTable(ctx, object, tableOptions) -} - -func (s *staticStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - return &s.info, nil -} diff --git a/pkg/registry/apis/example/subresource.go b/pkg/registry/apis/example/subresource.go deleted file mode 100644 index 3fa5807a9e9..00000000000 --- a/pkg/registry/apis/example/subresource.go +++ /dev/null @@ -1,54 +0,0 @@ -package example - -import ( - "context" - "fmt" - "net/http" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - "github.com/grafana/grafana/pkg/apimachinery/identity" - example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" -) - -type dummySubresourceREST struct{} - -var _ = rest.Connecter(&dummySubresourceREST{}) - -func (r *dummySubresourceREST) New() runtime.Object { - return &example.DummySubresource{} -} - -func (r *dummySubresourceREST) Destroy() { -} - -func (r *dummySubresourceREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (r *dummySubresourceREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" // true means you can use the trailing path as a variable -} - -func (r *dummySubresourceREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - user, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - - // This response object format is negotiated by k8s - dummy := &example.DummySubresource{ - Info: fmt.Sprintf("%s/%s", info.Value, user.GetLogin()), - } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - responder.Object(http.StatusOK, dummy) - }), nil -} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index d9782c44697..2ce96e4af0d 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" - "github.com/grafana/grafana/pkg/registry/apis/example" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/peakq" @@ -29,7 +28,6 @@ var WireSet = wire.NewSet( // Each must be added here *and* in the ServiceSink above playlist.RegisterAPIService, dashboard.RegisterAPIService, - example.RegisterAPIService, dashboardsnapshot.RegisterAPIService, featuretoggle.RegisterAPIService, datasource.RegisterAPIService, diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index 34ae2316813..a2c413d69a9 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry/apis/datasource" - "github.com/grafana/grafana/pkg/registry/apis/example" "github.com/grafana/grafana/pkg/registry/apis/featuretoggle" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/query/client" @@ -75,9 +74,6 @@ func (p *DummyAPIFactory) MakeAPIServer(_ context.Context, tracer tracing.Tracer } switch gv.Group { - case "example.grafana.app": - return example.NewTestingAPIBuilder(), nil - // Only works with testdata case "query.grafana.app": return query.NewQueryAPIBuilder( diff --git a/pkg/tests/apis/example/example_test.go b/pkg/tests/apis/example/example_test.go deleted file mode 100644 index c702402837f..00000000000 --- a/pkg/tests/apis/example/example_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package playlist - -import ( - "context" - "encoding/json" - "testing" - "time" - - "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" - - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/grafana/grafana/pkg/tests/testsuite" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestIntegrationExampleApp(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental APIs - DisableAnonymous: true, - EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service - }, - }) - - t.Run("Check runtime info resource", func(t *testing.T) { - // Resource is not namespaced! - client := helper.Org1.Admin.ResourceClient(t, schema.GroupVersionResource{ - Group: "example.grafana.app", - Version: "v0alpha1", - Resource: "runtime", - }) - rsp, err := client.List(context.Background(), metav1.ListOptions{}) - require.NoError(t, err) - - v, ok := rsp.Object["startupTime"].(int64) - require.True(t, ok) - require.Greater(t, v, time.Now().Add(-1*time.Hour).UnixMilli()) // should be within the last hour - }) - - t.Run("Check discovery client", func(t *testing.T) { - disco := helper.NewDiscoveryClient() - resources, err := disco.ServerResourcesForGroupVersion("example.grafana.app/v0alpha1") - require.NoError(t, err) - - v1Disco, err := json.MarshalIndent(resources, "", " ") - require.NoError(t, err) - //fmt.Printf("%s", string(v1Disco)) - - require.JSONEq(t, `{ - "kind": "APIResourceList", - "apiVersion": "v1", - "groupVersion": "example.grafana.app/v0alpha1", - "resources": [ - { - "name": "dummy", - "singularName": "dummy", - "namespaced": true, - "kind": "DummyResource", - "verbs": [ - "get", - "list" - ] - }, - { - "name": "dummy/sub", - "singularName": "", - "namespaced": true, - "kind": "DummySubresource", - "verbs": [ - "get" - ] - }, - { - "name": "runtime", - "singularName": "runtime", - "namespaced": false, - "kind": "RuntimeInfo", - "verbs": [ - "list" - ] - } - ] - }`, string(v1Disco)) - - //fmt.Printf("%s", string(v1Disco)) - require.JSONEq(t, `[ - { - "version": "v0alpha1", - "freshness": "Current", - "resources": [ - { - "resource": "dummy", - "responseKind": { - "group": "", - "kind": "DummyResource", - "version": "" - }, - "scope": "Namespaced", - "singularResource": "dummy", - "subresources": [ - { - "responseKind": { - "group": "", - "kind": "DummySubresource", - "version": "" - }, - "subresource": "sub", - "verbs": [ - "get" - ] - } - ], - "verbs": [ - "get", - "list" - ] - }, - { - "resource": "runtime", - "responseKind": { - "group": "", - "kind": "RuntimeInfo", - "version": "" - }, - "scope": "Cluster", - "singularResource": "runtime", - "verbs": [ - "list" - ] - } - ] - } - ]`, helper.GetGroupVersionInfoJSON("example.grafana.app")) - }) - - t.Run("Check dummy with subresource", func(t *testing.T) { - client := helper.Org1.Viewer.ResourceClient(t, schema.GroupVersionResource{ - Group: "example.grafana.app", - Version: "v0alpha1", - Resource: "dummy", - }).Namespace("default") - rsp, err := client.Get(context.Background(), "test2", metav1.GetOptions{}) - require.NoError(t, err) - - v, ok, err := unstructured.NestedString(rsp.Object, "spec", "Dummy") - require.NoError(t, err) - require.True(t, ok) - - require.Equal(t, "test2", v) - require.Equal(t, "DummyResource", rsp.GetObjectKind().GroupVersionKind().Kind) - - // Now a sub-resource - rsp, err = client.Get(context.Background(), "test2", metav1.GetOptions{}, "sub") - require.NoError(t, err) - - raw, err := json.MarshalIndent(rsp, "", " ") - require.NoError(t, err) - //fmt.Printf("%s", string(raw)) - require.JSONEq(t, `{ - "apiVersion": "example.grafana.app/v0alpha1", - "kind": "DummySubresource", - "info": "default/viewer-1" - }`, string(raw)) - }) -} From d972bdca085e12e93d733d23aa28438b92f7ca8b Mon Sep 17 00:00:00 2001 From: Jo Date: Fri, 21 Jun 2024 10:20:02 +0200 Subject: [PATCH 072/122] Docs: Add fixed role UUIDs to docs for terraform provisioning (#89457) * add role UUID to docs * add table description and terraform hyperlink --- .../index.md | 153 +++++++++--------- 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 3f75bca900a..1ec8a8a0b26 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -33,81 +33,84 @@ The following tables list permissions associated with basic and fixed roles. ## Fixed role definitions -| Fixed role | Permissions | Description | -| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `fixed:alerting.instances:writer` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:write` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | -| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | -| `fixed:alerting.notifications:writer` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | -| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting.rules:writer` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete`
`alert.silences:create`
`alert.silences:write` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and manage rule-specific silences | -| `fixed:alerting.rules:reader` | `alert.rule:read`, `alert.silences:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*`
`alert.notifications.time-intervals:read`
`alert.notifications.receivers:list` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and read rule-specific silences | -| `fixed:alerting:writer` | All permissions from `fixed:alerting.rules:writer`
`fixed:alerting.instances:writer`
`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting.provisioning.secrets:reader` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read-only permissions for Provisioning API and let export resources with decrypted secrets [\*](#alerting-roles) | -| `fixed:alerting.provisioning:writer` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) | -| `fixed:alerting.provisioning.status:writer` | `alert.provisioning.provenance:write` | Set provenance status to alert rules, notification policies, contact points, etc. Should be used together with regular writer roles. [\*](#alerting-roles) | -| `fixed:annotations.dashboard:writer` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | -| `fixed:annotations:reader` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | -| `fixed:annotations:writer` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | -| `fixed:apikeys:reader` | `apikeys:read` for scope `apikeys:*` | Read all api keys. | -| `fixed:apikeys:writer` | All permissions from `fixed:apikeys:reader` and
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | -| `fixed:authentication.config:writer` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | -| `fixed:dashboards:creator` | `dashboards:create`
`folders:read` | Create dashboards. | -| `fixed:dashboards.insights:reader` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. | -| `fixed:dashboards.permissions:reader` | `dashboards.permissions:read` | Read all dashboard permissions. | -| `fixed:dashboards.permissions:writer` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | -| `fixed:dashboards.public:writer` | `dashboards.public:write` | Create, update, delete or pause a public dashboard. | -| `fixed:dashboards:reader` | `dashboards:read` | Read all dashboards. | -| `fixed:dashboards:writer` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | -| `fixed:datasources.caching:reader` | `datasources.caching:read` | Read data source query caching settings. | -| `fixed:datasources.caching:writer` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | -| `fixed:datasources:explorer` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | -| `fixed:datasources.id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. | -| `fixed:datasources.insights:reader` | `datasources.insights:read` | Read data source insights data. | -| `fixed:datasources.permissions:reader` | `datasources.permissions:read` | Read data source permissions. | -| `fixed:datasources.permissions:writer` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | -| `fixed:datasources:creator` | `datasources:create` | Create data sources. | -| `fixed:datasources:reader` | `datasources:read`
`datasources:query` | Read and query data sources. | -| `fixed:datasources:writer` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | -| `fixed:folders.permissions:reader` | `folders.permissions:read` | Read all folder permissions. | -| `fixed:folders.permissions:writer` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | -| `fixed:folders:creator` | `folders:create` | Create folders in the root level. If granted together with `folders:write` permission, also allows creating subfolders under all folders. | -| `fixed:folders:reader` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | -| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. If granted together with `fixed:folders:creator`, allows creating subfolders under all folders. | -| `fixed:ldap:reader` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | -| `fixed:ldap:writer` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | -| `fixed:library.panels:creator` | `library.panels:create`
`folders:read` | Create library panel at the root level. | -| `fixed:library.panels:reader` | `library.panels:read` | Read all library panels. | -| `fixed:library.panels:general.reader` | `library.panels:read` | Read all library panels at the root level. | -| `fixed:library.panels:writer` | All permissions from `fixed:library.panels:reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions. | -| `fixed:library.panels:general.writer` | All permissions from `fixed:library.panels:general.reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions at the root level. | -| `fixed:licensing:reader` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | -| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | -| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. | -| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. | -| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | -| `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | -| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | -| `fixed:plugins.app:reader` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | -| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. Needs to be assigned globally. | -| `fixed:plugins:writer` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | -| `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | -| `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | -| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | -| `fixed:roles:reader` | `roles:read`
`teams.roles:read`
`users.roles:read`
`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. | -| `fixed:roles:writer` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | -| `fixed:roles:resetter` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | -| `fixed:serviceaccounts:reader` | `serviceaccounts:read` | Read Grafana service accounts. | -| `fixed:serviceaccounts:creator` | `serviceaccounts:create` | Create Grafana service accounts. | -| `fixed:serviceaccounts:writer` | `serviceaccounts:read`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. | -| `fixed:settings:reader` | `settings:read` | Read Grafana instance settings. | -| `fixed:settings:writer` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | -| `fixed:stats:reader` | `server.stats:read` | Read Grafana instance statistics. | -| `fixed:teams:reader` | `teams:read` | List all teams. | -| `fixed:teams:creator` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | -| `fixed:teams:writer` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | -| `fixed:users:reader` | `users:read`
`users.quotas:read`
`users.authtoken:read`
` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | -| `fixed:users:writer` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | +The following table has the existing built-in fixed role definitions. Other fixed roles might be added by plugins installed in Grafana. +The UUID presented here can be used as an identifier for [Terraform provisioning](../rbac-terraform-provisioning). + +| Fixed role | UUID | Permissions | Description | +| -------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fixed:alerting:reader` | `fixed_O2oP1_uBFozI2i93klAkcvEWR30` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting:writer` | `fixed_-PAZgSJsDlRD8NUg-PFSeH_BkJY` | All permissions from `fixed:alerting.rules:writer`
`fixed:alerting.instances:writer`
`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting.instances:reader` | `fixed_ut5fVS-Ulh_ejFoskFhJT_rYg0Y` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | +| `fixed:alerting.instances:writer` | `fixed_pKOBJE346uyqMLdgWbk1NsQfEl0` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:write` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | +| `fixed:alerting.notifications:reader` | `fixed_hmBn0lX5h1RZXB9Vaot420EEdA0` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting.notifications:writer` | `fixed_XplK6HPNxf9AP5IGTdB5Iun4tJc` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | +| `fixed:alerting.provisioning:writer` | `fixed_y7pFjdEkxpx5ETdcxPvp0AgRuUo` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) | +| `fixed:alerting.provisioning.secrets:reader` | `fixed_9fmzXXZZG-Od0Amy2ofEG8Uk--c` | `alert.provisioning:read` and `alert.provisioning.secrets:read` | Read-only permissions for Provisioning API and let export resources with decrypted secrets [\*](#alerting-roles) | +| `fixed:alerting.provisioning.status:writer` | `fixed_eAxlzfkTuobvKEgXHveFMBZrOj8` | `alert.provisioning.provenance:write` | Set provenance status to alert rules, notification policies, contact points, etc. Should be used together with regular writer roles. [\*](#alerting-roles) | +| `fixed:alerting.rules:reader` | `fixed_fRGKL_vAqUsmUWq5EYKnOha9DcA` | `alert.rule:read`, `alert.silences:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*`
`alert.notifications.time-intervals:read`
`alert.notifications.receivers:list` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and read rule-specific silences | +| `fixed:alerting.rules:writer` | `fixed_YJJGwAalUwDZPrXSyFH8GfYBXAc` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:write`
`alert.rule:delete`
`alert.silences:create`
`alert.silences:write` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) and manage rule-specific silences | +| `fixed:annotations:reader` | `fixed_hpZnoizrfAJsrceNcNQqWYV-xNU` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | +| `fixed:annotations:writer` | `fixed_ZVW-Aa9Tzle6J4s2aUFcq1StKWE` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | +| `fixed:annotations.dashboard:writer` | `fixed_8A775xenXeKaJk4Cr7bchP9yXOA` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | +| `fixed:apikeys:reader` | `fixed_kYZ7UEkwEvGmCCjTrq07cFAVFws` | `apikeys:read` for scope `apikeys:*` | Read all api keys. | +| `fixed:apikeys:writer` | `fixed_anTrcpRkm21NBO1Q2CsX8y0fiCQ` | All permissions from `fixed:apikeys:reader` and
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | +| `fixed:authentication.config:writer` | `fixed_0rYhZ2Qnzs8AdB1nX7gexk3fHDw` | `settings:read` for scope `settings:auth.saml:*`
`settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. | +| `fixed:dashboards:creator` | `fixed_ZorKUcEPCM01A1fPakEzGBUyU64` | `dashboards:create`
`folders:read` | Create dashboards. | +| `fixed:dashboards:reader` | `fixed_Sgr67JTOhjQGFlzYRahOe45TdWM` | `dashboards:read` | Read all dashboards. | +| `fixed:dashboards:writer` | `fixed_OK2YOQGIoI1G031hVzJB6rAJQAs` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | +| `fixed:dashboards.insights:reader` | `fixed_JlBJ2_gizP8zhgaeGE2rjyZe2Rs` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. | +| `fixed:dashboards.permissions:reader` | `fixed_f17oxuXW_58LL8mYJsm4T_mCeIw` | `dashboards.permissions:read` | Read all dashboard permissions. | +| `fixed:dashboards.permissions:writer` | `fixed_CcznxhWX_Yqn8uWMXMQ-b5iFW9k` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | +| `fixed:dashboards.public:writer` | `fixed_f_GHHRBciaqESXfGz2oCcooqHxs` | `dashboards.public:write` | Create, update, delete or pause a public dashboard. | +| `fixed:datasources:creator` | `fixed_XX8jHREgUt-wo1A-rPXIiFlX6Zw` | `datasources:create` | Create data sources. | +| `fixed:datasources:explorer` | `fixed_qDzW9mzx9yM91T5Bi8dHUM2muTw` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | +| `fixed:datasources:reader` | `fixed_C2x8IxkiBc1KZVjyYH775T9jNMQ` | `datasources:read`
`datasources:query` | Read and query data sources. | +| `fixed:datasources:writer` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | +| `fixed:datasources.caching:reader` | `fixed_D2ddpGxJYlw0mbsTS1ek9fj0kj4` | `datasources.caching:read` | Read data source query caching settings. | +| `fixed:datasources.caching:writer` | `fixed_JtFjHr7jd7hSqUYcktKvRvIOGRE` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | +| `fixed:datasources.id:reader` | `fixed_entg--fHmDqWY2-69N0ocawK0Os` | `datasources.id:read` | Read the ID of a data source based on its name. | +| `fixed:datasources.insights:reader` | `fixed_EBZ3NwlfecNPp2p0XcZRC1nfEYk` | `datasources.insights:read` | Read data source insights data. | +| `fixed:datasources.permissions:reader` | `fixed_ErYA-cTN3yn4h4GxaVPcawRhiOY` | `datasources.permissions:read` | Read data source permissions. | +| `fixed:datasources.permissions:writer` | `fixed_aiQh9YDfLOKjQhYasF9_SFUjQiw` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | +| `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. If granted together with `folders:write` permission, also allows creating subfolders under all folders. | +| `fixed:folders:reader` | `fixed_yeW-5QPeo-i5PZUIUXMlAA97GnQ` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | +| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. If granted together with `fixed:folders:creator`, allows creating subfolders under all folders. | +| `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. | +| `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | +| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | +| `fixed:ldap:writer` | `fixed_p6AvnU4GCQyIh7-hbwI-bk3GYnU` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | +| `fixed:library.panels:creator` | `fixed_6eX6ItfegCIY5zLmPqTDW8ZV7KY` | `library.panels:create`
`folders:read` | Create library panel at the root level. | +| `fixed:library.panels:general.reader` | `fixed_ct0DghiBWR_2BiQm3EvNPDVmpio` | `library.panels:read` | Read all library panels at the root level. | +| `fixed:library.panels:general.writer` | `fixed_DgprkmqfN_1EhZ2v1_d1fYG8LzI` | All permissions from `fixed:library.panels:general.reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions at the root level. | +| `fixed:library.panels:reader` | `fixed_tvTr9CnZ6La5vvUO_U_X1LPnhUs` | `library.panels:read` | Read all library panels. | +| `fixed:library.panels:writer` | `fixed_JTljAr21LWLTXCkgfBC4H0lhBC8` | All permissions from `fixed:library.panels:reader` plus
`library.panels:create`
`library.panels:delete`
`library.panels:write` | Create, read, write or delete all library panels and their permissions. | +| `fixed:licensing:reader` | `fixed_OADpuXvNEylO2Kelu3GIuBXEAYE` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | +| `fixed:licensing:writer` | `fixed_gzbz3rJpQMdaKHt-E4q0PVaKMoE` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | +| `fixed:org.users:reader` | `fixed_oCqNwlVHLOpw7-jAlwp4HzYqwGY` | `org.users:read` | Read users within a single organization. | +| `fixed:org.users:writer` | `fixed_VERj5nayasjgf_Yh0sWqqCkxWlw` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a new user, read information about a user and their role, remove a user from that organization, or change the role of a user. | +| `fixed:organization:maintainer` | `fixed_CMm-uuBaPUBf4r8XG3jIvxo55bg` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | +| `fixed:organization:reader` | `fixed_0SZPJlTHdNEe8zO91zv7Zwiwa2w` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | +| `fixed:organization:writer` | `fixed_Y4jGqDd8w1yCrPwlik8z5Iu8-3M` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | +| `fixed:plugins:maintainer` | `fixed_yEOKidBcWgbm74x-nTa3lW5lOyY` | `plugins:install` | Install and uninstall plugins. Needs to be assigned globally. | +| `fixed:plugins:writer` | `fixed_MRYpGk7kpNNwt2VoVOXFiPnQziE` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | +| `fixed:plugins.app:reader` | `fixed_AcZRiNYx7NueYkUqzw1o2OGGUAA` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | +| `fixed:provisioning:writer` | `fixed_bgk1FCyR6OEDwhgirZlQgu5LlCA` | `provisioning:reload` | Reload provisioning. | +| `fixed:reports:reader` | `fixed_72_8LU_0ukfm6BdblOw8Z9q-GQ8` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | +| `fixed:reports:writer` | `fixed_jBW3_7g1EWOjGVBYeVRwtFxhUNw` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | +| `fixed:roles:reader` | `fixed_GkfG-1NSwEGb4hpK3-E3qHyNltc` | `roles:read`
`teams.roles:read`
`users.roles:read`
`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. | +| `fixed:roles:resetter` | `fixed_WgPpC3qJRmVpVTJavFNwfS5RuzQ` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | +| `fixed:roles:writer` | `fixed_W5aFaw8isAM27x_eWfElBhZ0iOc` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | +| `fixed:serviceaccounts:creator` | `fixed_Ikw60fckA0MyiiZ73BawSfOULy4` | `serviceaccounts:create` | Create Grafana service accounts. | +| `fixed:serviceaccounts:reader` | `fixed_QFjJAZ88iawMLInYOxPA1DB1w6I` | `serviceaccounts:read` | Read Grafana service accounts. | +| `fixed:serviceaccounts:writer` | `fixed_iBvUNUEZBZ7PUW0vdkN5iojc2sk` | `serviceaccounts:read`
`serviceaccounts:create`
`serviceaccounts:write`
`serviceaccounts:delete`
`serviceaccounts.permissions:read`
`serviceaccounts.permissions:write` | Create, update, read and delete all Grafana service accounts and manage service account permissions. | +| `fixed:settings:reader` | `fixed_0LaUt1x6PP8hsZzEBhqPQZFUd8Q` | `settings:read` | Read Grafana instance settings. | +| `fixed:settings:writer` | `fixed_joIHDgMrGg790hMhUufVzcU4j44` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | +| `fixed:stats:reader` | `fixed_OnRCXxZVINWpcKvTF5A1gecJ7pA` | `server.stats:read` | Read Grafana instance statistics. | +| `fixed:teams:creator` | `fixed_nzVQoNSDSn0fg1MDgO6XnZX2RZI` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | +| `fixed:teams:reader` | `fixed_3SNL15gkRtJ7XeEKpMVJyQjYbjg` | `teams:read` | List all teams. | +| `fixed:teams:writer` | `fixed_xw1T0579h620MOYi4L96GUs7fZY` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | +| `fixed:users:reader` | `fixed_buZastUG3reWyQpPemcWjGqPAd0` | `users:read`
`users.quotas:read`
`users.authtoken:read`
` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | +| `fixed:users:writer` | `fixed_wjzgHHo_Ux25DJuELn_oiAdB_yM` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | ### Alerting roles From 5e75c0d17938e67f6b3fc33dfe1902eff658ae96 Mon Sep 17 00:00:00 2001 From: antonio <45235678+tonypowa@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:40:49 +0200 Subject: [PATCH 073/122] tutorials:alerting:get-started-p2 (#89477) * tutorials:alerting:get-started-p2 * author,figures * link, numbering, note * fix,title * Update docs/sources/tutorials/alerting-get-started-pt2/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/tutorials/alerting-get-started-pt2/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/tutorials/alerting-get-started-pt2/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * alt text * Revert "alt text" This reverts commit 511a946158ff8b88313d48f6d44b70e4703de42a. * alt-text-pt2 * all pretty no pity --------- Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- .../alerting-get-started-pt2/index.md | 172 ++++++++++++++++++ .../tutorials/alerting-get-started/index.md | 8 +- 2 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 docs/sources/tutorials/alerting-get-started-pt2/index.md diff --git a/docs/sources/tutorials/alerting-get-started-pt2/index.md b/docs/sources/tutorials/alerting-get-started-pt2/index.md new file mode 100644 index 00000000000..5a1ae5709e4 --- /dev/null +++ b/docs/sources/tutorials/alerting-get-started-pt2/index.md @@ -0,0 +1,172 @@ +--- +Feedback Link: https://github.com/grafana/tutorials/issues/new +categories: + - alerting +description: This is part 2 of the Get started with Grafana Alerting tutorials. Learn how to leverage alert instances, and set up a notification policy that routes alert notifications based on labels to a specific contact point. +id: alerting-get-started-pt2 +labels: + products: + - enterprise + - oss + - cloud +tags: + - beginner +title: Get started with Grafana Alerting - Part 2 +weight: 50 +--- + +# Get started with Grafana Alerting - Part 2 + +## Introduction + +This is part 2 of the [Get Started with Grafana Alerting tutorial](http://grafana.com/tutorials/alerting-get-started/). + +In this guide, we dig into more complex yet equally fundamental elements of Grafana Alerting: **alert instances** and **notification policies**. + +After introducing each component, you will learn how to: + +- Configure an alert rule that returns more than one alert instance +- Create notification policies that route firing alert instances to different contact points +- Use labels to match alert instances and notification policies + +Learning about alert instances and notification policies is useful if you have more than one contact point in your organization, or if your alert rule returns a number of metrics that you want to handle separately by routing each alert instance to a specific contact point. The tutorial will introduce each concept, followed by how to apply both concepts in a real-world scenario. + +## Alert instances + +An [alert instance](https://grafana.com/docs/grafana/latest/alerting/fundamentals/#alert-instances) is an event that matches a metric returned by an alert rule query. + +Let's consider a scenario where you're monitoring website traffic using Grafana. You've set up an alert rule to trigger an alert instance if the number of page views exceeds a certain threshold (more than `1000` page views) within a specific time period, say, over the past `5` minutes. + +If the query returns more than one time-series, each time-series represents a different metric or aspect being monitored. In this case, the alert rule is applied individually to each time-series. + +{{< figure alt="Screenshot displaying alert instances in the context of an alert rule, highlighting the specific alerts triggered by the rule and their respective statuses" src="/media/docs/alerting/get-started-digram-instance-grey.png" max-width="1200px" caption="Alert Instances in the Context of an Alert Rule" >}} + +In this scenario, each time-series is evaluated independently against the alert rule. It results in the creation of an alert instance for each time-series. The time-series corresponding to the desktop page views meets the threshold and, therefore, results in an alert instance in **Firing** state for which an alert notification is sent. The mobile alert instance state remains **Normal**. + +## Notification policies + +[Notification policies](https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/notification-policies/) route alerts to different communication channels, reducing alert noise and providing control over when and how alerts are sent. For example, you might use notification policies to ensure that critical alerts about server downtime are sent immediately to the on-call engineer. Another use case could be routing performance alerts to the development team for review and action. + +Key Characteristics: + +- Route alert notifications by matching alerts and policies with labels +- Manage when to send notifications + +{{< figure alt="Screenshot illustrating the routing of alerts with notification policies, including the configuration and flow of alerts through different notification channels" src="/media/docs/alerting/get-started-notification-policy-tree-combo.png" max-width="1200px" caption="Routing of alerts with notification policies" >}} + +In the above diagram, alert instances and notification policies are matched by labels. For instance, the label `team=operations` matches the alert instance “**Pod stuck in CrashLoop**” and “**Disk Usage -80%**” to child policies that send alert notifications to a particular contact point (operations@grafana.com). + +## Create notification policies + +Create a notification policy if you want to handle metrics returned by alert rules separately by routing each alert instance to a specific contact point. In Grafana, click on the icon at the top left corner of the screen to access the navigation menu. + +1. Navigate to **Alerts & IRM > Alerting > Notification policies**. +1. In the Default policy, click **+ New child policy**. +1. In the field **Label** enter `device`, and in the field **Value** enter `desktop`. +1. From the **Contact point** drop-down, choose **Webhook**. + {{< admonition type="note" >}} + If you don’t have any contact points, add a [Contact point](http://localhost:3002/docs/grafana/latest/tutorials/alerting-get-started/#create-a-contact-point). + {{}} +1. Click **Save Policy**. + + This new child policy routes alerts that match the label `device=desktop` to the Webhook contact point. + +1. **Repeat the steps above to create a second child policy** to match another alert instance. For labels use: `device=mobile`. Use the Webhook integration for the contact point. Alternatively, experiment by using a different Webhook endpoint or a [different integration](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/#list-of-supported-integrations). + +## Create an alert rule that returns alert instances + +The alert rule that you are about to create is meant to monitor web traffic page views. The objective is to explore what an alert instance is and how to leverage routing individual alert instances by using label matchers and notification policies. + +### Add a data source + +Grafana includes a [test data source](https://grafana.com/docs/grafana/latest/datasources/testdata/) that creates simulated time series data. + +1. In Grafana navigate to **Connections > Add new connection**. +1. Search for **TestData**. +1. Click **Add new data source**. +1. Click **Save & test**. + + You should see a message confirming that the data source is working. + +### Create an alert rule + +1. Navigate to **Alerting > Alert rules**. +1. Click **New alert rule**. + +### Enter an alert rule name + +Make it short and descriptive as this will appear in your alert notification. For instance, `web-traffic`. + +### Define query and alert condition + +In this section, we define queries, expressions (used to manipulate the data), and the condition that must be met for the alert to be triggered. + +1. Select **TestData** data source from the drop-down menu. +1. From **Scenario** select **CSV Content**. +1. Copy in the following CSV data: + +``` +device,views +desktop,1200 +mobile,900 +``` + +The above CSV data simulates a data source returning multiple time series, each leading to the creation of an alert instance for that specific time series. Note that the data returned matches the example in the [Alert instance](#alert-instances) section. + +1. Remove the ‘B’ **Reduce expression** (click the bin icon). The Reduce expression is default, and in this case, is not required since the queried data is already reduced. Note that the Threshold expression is now your **Alert condition**. +1. In the ‘C’ **Threshold expression**: + - Change the **Input** to ‘**A**’ to select the data source. + - Enter `1000` as the threshold value. This is the value above which the alert rule should trigger. +1. Click **Preview** to run the queries. + +It should return two series.`desktop` in Firing state, and `mobile` in Normal state. The values `1`, and `0` mean that the condition is either `true` or `false`. + +{{< figure alt="Screenshot showing a preview of a query in Grafana that returns two alert instances, including the query results and relevant alert details" src="/media/docs/alerting/get-started-expression-instances.png" max-width="1200px" caption="Preview of a query returning two alert instances in Grafana." >}} + +### Set evaluation behavior + +In the [life cycle](http://grafana.com/docs/grafana/next/alerting/fundamentals/alert-rule-evaluation/) of alert instances, when an alert condition (threshold) is not met, the alert instance state is **Normal**. Similarly, when the condition is breached (for longer than the pending period, which in this tutorial will be 0), the alert instance state switches back to **Alerting**, which means that the alert rule state is **Firing**, and a notification is sent. + +To set up evaluation behavior: + +1. In **Folder**, click **+ New folder** and enter a name. For example: `web-traffic-alerts`. This folder will contain our alerts. +1. In the **Evaluation group**, repeat the above step to create a new evaluation group. We will name it `1m` (referring to “1 minute”). +1. Choose an Evaluation interval (how often the alert will be evaluated). Choose `1m`. +1. Set the pending period to `0s` (zero seconds), so the alert rule fires the moment the condition is met. + +### Configure labels and notifications + +In this section, you can select how you want to route your alert instances. Since we want to route by notification policy, we need to ensure that the labels match the alert instance. + +1. Choose **Use notification policy**. +1. Click **Preview routing**. Based on the existing labels, you should see a preview of what policies are matching with the alerts. There should be two alert instances matching the labels that were previously setup in each notification policy: `device=desktop`, `device=mobile`. + + These [types of labels](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/annotation-label/#label-types) are generated by the data source query and they can be leveraged to match our notification policies without needing to manually add them to the alert rule. + + {{< figure alt="Screenshot showing a routing preview of matched notification policies, detailing how alerts are matched and routed to specific notification channels" src="/media/docs/alerting/get-started-alert-instace-routing-prev.png" max-width="1200px" caption="Routing preview of matched notification policies" >}} + + {{< admonition type="note" >}} + Even if both labels match the policies, only the alert instance in Firing state produces an alert notification. + {{}} + +1. Click **Save rule and exit**. + +Now that we have set up the alert rule, it’s time to check the alert notification. + +## Receive alert notifications + +Now that the alert rule has been configured, you should receive alert [notifications](http://grafana.com/docs/grafana/next/alerting/fundamentals/alert-rule-evaluation/state-and-health/#notifications) in the contact point whenever the alert triggers and gets resolved. In our example, each alert instance should be routed separately as we configured labels to match notification policies. Once the evaluation interval has concluded (1m), you should receive an alert notification in the Webhook endpoint. + +{{< figure alt="Screenshot showing the exploration of alert notification details in a webhook endpoint, displaying the content and structure of the alert payload received by the endpoint" src="/media/docs/alerting/get-started-webhook-alert-isntance.png" max-width="1200px" caption="Exploring alert notification details in webhook endpoint" >}} + +The alert notification details show that the alert instance corresponding to the website views from desktop devices was correctly routed through the notification policy to the Webhook contact point. The notification also shows that the instance is in **Firing** state, as well as it includes the label `device=desktop`, which makes the routing of the alert instance possible. + +Feel free to change the CSV data in the alert rule to trigger the routing of the alert instance that matches the label `device=mobile`. + +## Summary + +In this tutorial, you have learned how Grafana Alerting can route individual alert instances using the labels generated by the data-source query and match these labels with notification policies, which in turn routes alert notifications to specific contact points. + +If you run into any problems, you are welcome to post questions in our [Grafana Community forum](https://community.grafana.com/). + +Enjoy your monitoring! diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index 18ac15cee5b..15357f0fa0a 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -1,11 +1,9 @@ --- Feedback Link: https://github.com/grafana/tutorials/issues/new -authors: - - antonio-calero-merello categories: - alerting description: Get started with Grafana Alerting by creating your first alert in just a few minutes. Learn how to set up an alert, send alert notifications to a public webhook, and generate sample data to observe your alert in action. -id: alerting-get-started +id: alerting-get-started-pt1 labels: products: - enterprise @@ -13,11 +11,11 @@ labels: - cloud tags: - beginner -title: Get started with Grafana Alerting +title: Get started with Grafana Alerting - Part 1 weight: 50 --- -# Get Started with Grafana Alerting +# Get Started with Grafana Alerting - Part 1 In this guide, we'll walk you through the process of setting up your first alert in just a few minutes. You'll witness your alert in action with real-time data, as well as sending alert notifications. From 88404715748e858c30bb457f6d9fa32eb368218b Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 21 Jun 2024 09:48:40 +0100 Subject: [PATCH 074/122] Internationalisation: Update docs with nested variable examples (#89484) * update i18n docs to handle interpolating variables inside inner components * update with wrapping component example --- contribute/internationalization.md | 30 +++++++++++++++++++++++-- packages/grafana-eslint-rules/README.md | 21 +++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/contribute/internationalization.md b/contribute/internationalization.md index 5ca730ab835..8729f8175f5 100644 --- a/contribute/internationalization.md +++ b/contribute/internationalization.md @@ -22,14 +22,40 @@ Grafana uses the [i18next](https://www.i18next.com/) framework for managing tran ```jsx import { Trans } from 'app/core/internationalization'; +const SearchTitle = ({ term }) => Results for {{ term }}; +``` + +Prefer using `` for JSX children, and `t()` for props and other JavaScript usage. + +There may be cases where you need to interpolate variables inside other components in the translation. + +If the nested component is displaying the variable only (e.g. to add emphasis or color), the best solution is to create a new wrapping component: + +```jsx +import { Trans } from 'app/core/internationalization'; +import { Text } from '@grafana/ui'; + +const SearchTerm = ({ term }) => {term}; + const SearchTitle = ({ term }) => ( - Results for {{ term }} + Results for ); ``` -Prefer using `` for JSX children, and `t()` for props and other JavaScript usage. +However there are also cases where the nested component might be displaying additional text which also needs to be translated. In this case, you can use the `values` prop to explicitly pass variables to the translation, and reference them as templated strings in the markup. For example: + +```jsx +import { Trans } from 'app/core/internationalization'; +import { Text } from '@grafana/ui'; + +const SearchTitle = ({ term }) => ( + + Results for {'{{ myVariable }}'} and this translated text is also in green + +); +``` When translating in `grafana-ui`, use a relative path to import `` and `t()` from `src/utils/i18n`. diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index 060db7211d9..2f73d46a529 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -133,16 +133,23 @@ Check if strings are marked for translation. ```tsx // Bad ❌ -const SearchTitle = ({ term }) => ( -
- Results for {term} -
-); +const SearchTitle = ({ term }) =>
Results for {term}
; -//Good ✅ +// Good ✅ +const SearchTitle = ({ term }) => Results for {{ term }}; + +// Good ✅ (if you need to interpolate variables inside nested components) +const SearchTerm = ({ term }) => {term}; const SearchTitle = ({ term }) => ( - Results for {{ term }} + Results for + +); + +// Good ✅ (if you need to interpolate variables and additional translated strings inside nested components) +const SearchTitle = ({ term }) => ( + + Results for {'{{ myVariable }}'} and this translated text is also in green ); ``` From be65aaf5dcc212b85db94969c0735eb0443b01b5 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 21 Jun 2024 10:03:57 +0100 Subject: [PATCH 075/122] Chore: Clean up some more old SCSS (#89404) * remove/migrate normalize.scss * fix typo * migrate _add_data_source styles * migrate panel-header styles * migrate slate styles to emotion globals * put back a couple of classes used by external plugins --- .../src/themes/GlobalStyles/GlobalStyles.tsx | 2 + .../src/themes/GlobalStyles/slate.ts | 145 +++++++++++++++ .../PanelEditor/PanelHeaderCorner.test.tsx | 4 +- .../PanelEditor/PanelHeaderCorner.tsx | 34 +++- .../components/DataSourceCategories.tsx | 26 ++- public/sass/_angular.scss | 71 ++++++++ public/sass/_grafana.scss | 3 - public/sass/components/_add_data_source.scss | 86 --------- public/sass/components/_panel_header.scss | 122 ------------- public/sass/components/_slate_editor.scss | 165 ------------------ 10 files changed, 269 insertions(+), 389 deletions(-) create mode 100644 packages/grafana-ui/src/themes/GlobalStyles/slate.ts delete mode 100644 public/sass/components/_add_data_source.scss delete mode 100644 public/sass/components/_panel_header.scss delete mode 100644 public/sass/components/_slate_editor.scss diff --git a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx index e8880ad6f5e..5a437110ab5 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx +++ b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx @@ -17,6 +17,7 @@ import { getMarkdownStyles } from './markdownStyles'; import { getPageStyles } from './page'; import { getRcTimePickerStyles } from './rcTimePicker'; import { getSkeletonStyles } from './skeletonStyles'; +import { getSlateStyles } from './slate'; import { getUplotStyles } from './uPlot'; /** @internal */ @@ -38,6 +39,7 @@ export function GlobalStyles() { getAgularPanelStyles(theme), getMarkdownStyles(theme), getSkeletonStyles(theme), + getSlateStyles(theme), getRcTimePickerStyles(theme), getUplotStyles(theme), getLegacySelectStyles(theme), diff --git a/packages/grafana-ui/src/themes/GlobalStyles/slate.ts b/packages/grafana-ui/src/themes/GlobalStyles/slate.ts new file mode 100644 index 00000000000..e8fd858d456 --- /dev/null +++ b/packages/grafana-ui/src/themes/GlobalStyles/slate.ts @@ -0,0 +1,145 @@ +import { css } from '@emotion/react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +export function getSlateStyles(theme: GrafanaTheme2) { + return css({ + '.slate-query-field': { + fontSize: theme.typography.fontSize, + fontFamily: theme.typography.fontFamilyMonospace, + height: 'auto', + wordBreak: 'break-word', + // Affects only placeholder in query field. Adds scrollbar only if content is cropped. + overflow: 'auto', + }, + + '.slate-query-field__wrapper': { + position: 'relative', + display: 'inline-block', + padding: '6px 8px', + minHeight: '32px', + width: '100%', + color: theme.colors.text.primary, + backgroundColor: theme.components.input.background, + backgroundImage: 'none', + border: `1px solid ${theme.components.input.borderColor}`, + borderRadius: theme.shape.radius.default, + transition: 'all 0.3s', + lineHeight: '18px', + }, + + '.slate-query-field__wrapper--disabled': { + backgroundColor: 'inherit', + cursor: 'not-allowed', + }, + + '.slate-typeahead': { + '.typeahead': { + position: 'relative', + zIndex: theme.zIndex.typeahead, + borderRadius: theme.shape.radius.default, + border: `1px solid ${theme.components.panel.borderColor}`, + maxHeight: '66vh', + overflowY: 'scroll', + overflowX: 'hidden', + outline: 'none', + listStyle: 'none', + background: theme.components.panel.background, + color: theme.colors.text.primary, + boxShadow: theme.shadows.z2, + }, + + '.typeahead-group__title': { + color: theme.colors.text.secondary, + fontSize: theme.typography.size.sm, + lineHeight: theme.typography.body.lineHeight, + padding: theme.spacing(1), + }, + + '.typeahead-item': { + height: 'auto', + fontFamily: theme.typography.fontFamilyMonospace, + padding: theme.spacing(1, 1, 1, 2), + fontSize: theme.typography.size.sm, + textOverflow: 'ellipsis', + overflow: 'hidden', + zIndex: 1, + display: 'block', + whiteSpace: 'nowrap', + cursor: 'pointer', + transition: + 'color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1)', + }, + + '.typeahead-item__selected': { + backgroundColor: theme.isDark ? theme.v1.palette.dark9 : theme.v1.palette.gray6, + + '.typeahead-item-hint': { + fontSize: theme.typography.size.xs, + color: theme.colors.text.primary, + whiteSpace: 'normal', + }, + }, + + '.typeahead-match': { + color: theme.v1.palette.yellow, + borderBottom: `1px solid ${theme.v1.palette.yellow}`, + // Undoing mark styling + padding: 'inherit', + background: 'inherit', + }, + }, + + /* SYNTAX */ + + '.slate-query-field, .prism-syntax-highlight': { + '.token.comment, .token.block-comment, .token.prolog, .token.doctype, .token.cdata': { + color: theme.colors.text.secondary, + }, + + '.token.variable, .token.entity': { + color: theme.colors.text.primary, + }, + + '.token.property, .token.tag, .token.constant, .token.symbol, .token.deleted': { + color: theme.colors.error.text, + }, + + '.token.attr-value, .token.selector, .token.string, .token.char, .token.builtin, .token.inserted': { + color: theme.colors.success.text, + }, + + '.token.boolean, .token.number, .token.operator, .token.url': { + color: '#fe85fc', + }, + + '.token.function, .token.attr-name, .token.function-name, .token.atrule, .token.keyword, .token.class-name': { + color: theme.colors.primary.text, + }, + + '.token.punctuation, .token.regex, .token.important': { + color: theme.v1.palette.orange, + }, + + '.token.important': { + fontWeight: 'normal', + }, + + '.token.bold': { + fontWeight: 'bold', + }, + + '.token.italic': { + fontStyle: 'italic', + }, + + '.token.entity': { + cursor: 'help', + }, + + '.namespace': { + opacity: 0.7, + }, + }, + }); +} diff --git a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx index c92baacc746..5e2067ce961 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.test.tsx @@ -1,16 +1,18 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; +import { createTheme } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { PanelModel } from '../../state'; -import PanelHeaderCorner, { Props } from './PanelHeaderCorner'; +import { PanelHeaderCorner, Props } from './PanelHeaderCorner'; const setup = () => { const testPanel = new PanelModel({ title: 'test', description: 'test panel' }); const props: Props = { panel: testPanel, + theme: createTheme(), }; return render(); }; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx index c761570fdc4..1d5c7a5a8e1 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx @@ -5,7 +5,7 @@ import { renderMarkdown, LinkModelSupplier, ScopedVars, IconName } from '@grafan import { GrafanaTheme2 } from '@grafana/data/'; import { selectors } from '@grafana/e2e-selectors'; import { locationService, getTemplateSrv } from '@grafana/runtime'; -import { Tooltip, PopoverContent, Icon } from '@grafana/ui'; +import { Tooltip, PopoverContent, Icon, Themeable2, withTheme2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui/'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; @@ -17,7 +17,7 @@ enum InfoMode { Links = 'Links', } -export interface Props { +export interface Props extends Themeable2 { panel: PanelModel; title?: string; description?: string; @@ -45,22 +45,23 @@ export class PanelHeaderCorner extends Component { }; getInfoContent = (): JSX.Element => { - const { panel } = this.props; + const { panel, theme } = this.props; const markdown = panel.description || ''; const interpolatedMarkdown = getTemplateSrv().replace(markdown, panel.scopedVars); const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown); const links = this.props.links && this.props.links.getLinks(panel.replaceVariables); + const styles = getContentStyles(theme); return ( -
+
{links && links.length > 0 && ( -