diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 39a480c8a85..77b60baf905 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -48,7 +48,7 @@ /docs/sources/whatsnew/ @jtvdez -/docs/sources/developer-resources/plugins/ @grafana/plugins-platform-frontend @grafana/plugins-platform-backend +/docs/sources/developers/plugins/ @grafana/plugins-platform-frontend @grafana/plugins-platform-backend /docs/sources/visualizations/dashboards/ @imatwawana /docs/sources/visualizations/panels-visualizations/ @imatwawana @@ -89,6 +89,7 @@ /apps/folder/ @grafana/grafana-app-platform-squad /apps/playlist/ @grafana/grafana-app-platform-squad /apps/plugins/ @grafana/plugins-platform-backend +/apps/collections/ @grafana/grafana-app-platform-squad @grafana/grafana-frontend-platform /apps/preferences/ @grafana/grafana-app-platform-squad @grafana/grafana-frontend-platform /apps/shorturl/ @grafana/sharing-squad /apps/secret/ @grafana/grafana-operator-experience-squad @@ -743,6 +744,14 @@ i18next.config.ts @grafana/grafana-frontend-platform # @grafana/api-clients /packages/grafana-api-clients/ @grafana/grafana-frontend-platform @grafana/grafana-search-navigate-organise +/packages/grafana-api-clients/src/clients/rtkq/advisor/ @grafana/plugins-platform-frontend +/packages/grafana-api-clients/src/clients/rtkq/correlations/ @grafana/datapro +/packages/grafana-api-clients/src/clients/rtkq/dashboard/ @grafana/dashboards-squad +/packages/grafana-api-clients/src/clients/rtkq/folder/ @grafana/grafana-search-navigate-organise +/packages/grafana-api-clients/src/clients/rtkq/iam/ @grafana/access-squad @grafana/identity-squad +/packages/grafana-api-clients/src/clients/rtkq/preferences/ @grafana/plugins-platform-frontend +/packages/grafana-api-clients/src/clients/rtkq/provisioning/ @grafana/grafana-git-ui-sync-team +/packages/grafana-api-clients/src/clients/rtkq/shorturl/ @grafana/sharing-squad # root files, mostly frontend /.browserslistrc @grafana/frontend-ops diff --git a/.golangci.yml b/.golangci.yml index 33b2b9e8a19..3b7871662e2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -181,7 +181,9 @@ linters: - pkg: io/ioutil desc: 'Deprecated: As of Go 1.16, the same functionality is now provided by package io or package os, and those implementations should be preferred in new code. See the specific function documentation for details.' - pkg: gopkg.in/yaml.v2 - desc: Grafana packages are not allowed to depend on gopkg.in/yaml.v2 as gopkg.in/yaml.v3 is now available + desc: use go.yaml.in/yaml/v3 instead + - pkg: gopkg.in/yaml.v3 + desc: use go.yaml.in/yaml/v3 instead - pkg: github.com/pkg/errors desc: 'Deprecated: Go 1.13 supports the functionality provided by pkg/errors in the standard library.' - pkg: github.com/xorcare/pointer diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e53f42f32..a4dfcb3171a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,251 @@ + + +# 12.3.0 (2025-11-19) + +### Features and enhancements + +- **API Clients:** Add lazy hooks to clients [#113226](https://github.com/grafana/grafana/pull/113226), [@tomratcliffe](https://github.com/tomratcliffe) +- **API clients:** Automatically set PATCH headers [#111879](https://github.com/grafana/grafana/pull/111879), [@Clarity-89](https://github.com/Clarity-89) +- **API clients:** Extract into a package [#111810](https://github.com/grafana/grafana/pull/111810), [@Clarity-89](https://github.com/Clarity-89) +- **API clients:** Extract into a package (Enterprise) +- **API clients:** Update API clients to include all endpoints & add hooks [#113061](https://github.com/grafana/grafana/pull/113061), [@tomratcliffe](https://github.com/tomratcliffe) +- **AccessControl:** Include hidden roles in service account role display [#112924](https://github.com/grafana/grafana/pull/112924), [@Jguer](https://github.com/Jguer) +- **AccessControl:** Increase limit of LBAC for Datasources rules [#111560](https://github.com/grafana/grafana/pull/111560), [@Jguer](https://github.com/Jguer) +- **Accessibility:** Wrap data source info onto 2 lines at small viewports [#113033](https://github.com/grafana/grafana/pull/113033), [@ashharrison90](https://github.com/ashharrison90) +- **Alert Enrichment:** Add mutator to insert rule UID labels to allow for efficient use of labelSelector (Enterprise) +- **Alerting:** Add enrichment components to rule view page (Enterprise) +- **Alerting:** Add enrichment section to rule view page (Enterprise) +- **Alerting:** Add jitter support for periodic alert state storage to reduce database load spikes [#111357](https://github.com/grafana/grafana/pull/111357), [@softho0n](https://github.com/softho0n) +- **Alerting:** Add position-based matching for identical alert rules [#112407](https://github.com/grafana/grafana/pull/112407), [@konrad147](https://github.com/konrad147) +- **Alerting:** Create alertingAlertRuleFormSchema in restrictedGrafanaApis [#112794](https://github.com/grafana/grafana/pull/112794), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Display error message in central state history view [#111445](https://github.com/grafana/grafana/pull/111445), [@laurenashleigh](https://github.com/laurenashleigh) +- **Alerting:** Enrichment per rule wip-2 (Enterprise) +- **Alerting:** Hide metadata if grouping by folder [#113216](https://github.com/grafana/grafana/pull/113216), [@laurenashleigh](https://github.com/laurenashleigh) +- **Alerting:** Improve template ai helper prompt and add some examples (Enterprise) +- **Alerting:** Move enrichment tab between details and versions [#110886](https://github.com/grafana/grafana/pull/110886), [@laurenashleigh](https://github.com/laurenashleigh) +- **Alerting:** Remove ai feedback button from alert form [#112713](https://github.com/grafana/grafana/pull/112713), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Remove unused components [#111320](https://github.com/grafana/grafana/pull/111320), [@laurenashleigh](https://github.com/laurenashleigh) +- **Alerting:** Remove useRulesSourcesWithRuler for SmartAlertTypeDetector [#111623](https://github.com/grafana/grafana/pull/111623), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Surface remote AM silence creation errors properly [#112757](https://github.com/grafana/grafana/pull/112757), [@moustafab](https://github.com/moustafab) +- **Alerting:** Triage [#110339](https://github.com/grafana/grafana/pull/110339), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Triage rule details drawer [#112055](https://github.com/grafana/grafana/pull/112055), [@konrad147](https://github.com/konrad147) +- **Alerting:** Update prompt examples for template AI Helper (Enterprise) +- **Alerting:** Update width to instance details drawer in Triage page [#113209](https://github.com/grafana/grafana/pull/113209), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Use new enrichment endpoints in FE (Enterprise) +- **Alerting:** Use ruleUid as a prop instead of extracting it from the rule context (Enterprise) +- **Analytics:** Aggregate daily summary in datasources analytics (Enterprise) +- **Analytics:** Apply proper batching to Loki exports and add configurable settings (Enterprise) +- **Annotations:** Exclude internal dashboard id when saved via UID [#111535](https://github.com/grafana/grafana/pull/111535), [@ryantxu](https://github.com/ryantxu) +- **Azure:** Use SSO settings in plugin context [#112058](https://github.com/grafana/grafana/pull/112058), [@aangelisc](https://github.com/aangelisc) +- **Buttons:** Active style for buttons [#111235](https://github.com/grafana/grafana/pull/111235), [@gtk-grafana](https://github.com/gtk-grafana) +- **Caching:** Disable cache if datasource has oauthPassThru=true (Enterprise) +- **Canvas:** Allow non-icon bg image fields [#112308](https://github.com/grafana/grafana/pull/112308), [@fastfrwrd](https://github.com/fastfrwrd) +- **Chore:** Add logsdrilldown replace to apps/iam/go.mod [#112581](https://github.com/grafana/grafana/pull/112581), [@njvrzm](https://github.com/njvrzm) +- **CloudWatch Logs:** Don't add console link to every field in the logs response [#112230](https://github.com/grafana/grafana/pull/112230), [@idastambuk](https://github.com/idastambuk) +- **CloudWatch Logs:** Support Log Anomalies query type [#113067](https://github.com/grafana/grafana/pull/113067), [@idastambuk](https://github.com/idastambuk) +- **CloudWatch:** Add syntax highlighting and autocomplete for logs diff command [#111207](https://github.com/grafana/grafana/pull/111207), [@kevinwcyu](https://github.com/kevinwcyu) +- **CloudWatch:** Add tracking for logs anomalies [#113181](https://github.com/grafana/grafana/pull/113181), [@idastambuk](https://github.com/idastambuk) +- **Dashboard Controls:** Add annotations to the dashboard controls menu [#112816](https://github.com/grafana/grafana/pull/112816), [@leventebalogh](https://github.com/leventebalogh) +- **Dashboard Picker:** Update to use correct search + dashboards APIs [#112341](https://github.com/grafana/grafana/pull/112341), [@tomratcliffe](https://github.com/tomratcliffe) +- **Dashboard:** Backend always set `metricEditorMode: 0` regardless `metricQueryType` and `expression` [#111613](https://github.com/grafana/grafana/pull/111613), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Dashboards:** Add a new variable type called "Switch" [#111366](https://github.com/grafana/grafana/pull/111366), [@leventebalogh](https://github.com/leventebalogh) +- **Dashboards:** Hide error notifications in kiosk mode on dashboards [#112390](https://github.com/grafana/grafana/pull/112390), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Dynamic Dashboards:** Expand dashboards_init_dashboard_completed tracking info [#111102](https://github.com/grafana/grafana/pull/111102), [@idastambuk](https://github.com/idastambuk) +- **ErrorBoundary:** Report specific boundary type to Faro [#112071](https://github.com/grafana/grafana/pull/112071), [@tskarhed](https://github.com/tskarhed) +- **Explore:** Use compact mode only when targeting Tempo [#113037](https://github.com/grafana/grafana/pull/113037), [@ifrost](https://github.com/ifrost) +- **FeatureToggles:** Remove deprecated experimental apiserver [#111617](https://github.com/grafana/grafana/pull/111617), [@ryantxu](https://github.com/ryantxu) +- **Fields Selector:** Add component and integrate with Logs and Logs table visualization [#112534](https://github.com/grafana/grafana/pull/112534), [@matyax](https://github.com/matyax) +- **Flame Graph:** Anchor exact match when clicking a table symbol in search [#111101](https://github.com/grafana/grafana/pull/111101), [@samarthbagga-meesho](https://github.com/samarthbagga-meesho) +- **FlameGraph:** Improve prompt for open assistant to analyze flamegraph [#113071](https://github.com/grafana/grafana/pull/113071), [@simonswine](https://github.com/simonswine) +- **FolderPicker:** Don't show expand button for empty folders and move search icon [#111872](https://github.com/grafana/grafana/pull/111872), [@aocenas](https://github.com/aocenas) +- **FolderPicker:** Show parent folder when searching [#111026](https://github.com/grafana/grafana/pull/111026), [@aocenas](https://github.com/aocenas) +- **Geomap:** Add a MapLibre style base layer [#109841](https://github.com/grafana/grafana/pull/109841), [@remogeissbuehler](https://github.com/remogeissbuehler) +- **Geomap:** Move beta layers to GA [#113186](https://github.com/grafana/grafana/pull/113186), [@drew08t](https://github.com/drew08t) +- **Go:** Update to 1.25.2 + golangci-lint v2.5.0 + golang.org/x/net v0.45.0 [#112149](https://github.com/grafana/grafana/pull/112149), [@macabu](https://github.com/macabu) +- **Go:** Update to 1.25.3 [#112359](https://github.com/grafana/grafana/pull/112359), [@macabu](https://github.com/macabu) +- **Grafana Advisor:** Prometheus Type Migration check [#110853](https://github.com/grafana/grafana/pull/110853), [@bossinc](https://github.com/bossinc) +- **Grafana Data Source:** Add random walk configuration options [#113009](https://github.com/grafana/grafana/pull/113009), [@nmarrs](https://github.com/nmarrs) +- **IAM:** Add uid column in team_member DB table [#112439](https://github.com/grafana/grafana/pull/112439), [@dmihai](https://github.com/dmihai) +- **Jaeger:** Migrate API calls to gRPC endpoint [#113297](https://github.com/grafana/grafana/pull/113297), [@jcolladokuri](https://github.com/jcolladokuri) +- **LBAC for data sources:** Provide user feedback of potential performance loss from LBAC rules (Enterprise) +- **Library Panels:** Remove direct use of legacy search [#112231](https://github.com/grafana/grafana/pull/112231), [@tomratcliffe](https://github.com/tomratcliffe) +- **Logs panel:** Respect selected fields for downloading logs [#111753](https://github.com/grafana/grafana/pull/111753), [@matyax](https://github.com/matyax) +- **Nav:** Render menu items as `p` tags so truncation logic can work [#113248](https://github.com/grafana/grafana/pull/113248), [@tomratcliffe](https://github.com/tomratcliffe) +- **Navigation:** Move Cost management and billing plugin to root [#111739](https://github.com/grafana/grafana/pull/111739), [@gubjanos](https://github.com/gubjanos) +- **PanelTimeCompare:** Support saving time compare window [#113150](https://github.com/grafana/grafana/pull/113150), [@torkelo](https://github.com/torkelo) +- **PanelTimeSettings:** Support panel time range settings changes from dashboard in view mode [#113027](https://github.com/grafana/grafana/pull/113027), [@torkelo](https://github.com/torkelo) +- **Plugins:** Install Grafana Pathfinder behind a feature flag [#109909](https://github.com/grafana/grafana/pull/109909), [@Jayclifford345](https://github.com/Jayclifford345) +- **PostgreSQL:** Support PGPASSFILE by making password optional [#108856](https://github.com/grafana/grafana/pull/108856), [@taraspos](https://github.com/taraspos) +- **Provisioning:** Watch file system for changes [#112184](https://github.com/grafana/grafana/pull/112184), [@ryantxu](https://github.com/ryantxu) +- **Reporting:** Add support for schema v2 dashboards (Enterprise) +- **Reporting:** Wait for streaming to end before exporting CSVs (Enterprise) +- **SQL Expressions:** Add Functions to Allow list [#113291](https://github.com/grafana/grafana/pull/113291), [@kylebrandt](https://github.com/kylebrandt) +- **Snapshots:** Use appSubUrl for View all snapshots [#111652](https://github.com/grafana/grafana/pull/111652), [@Clarity-89](https://github.com/Clarity-89) +- **Span Details:** Bring back span id to span details [#112411](https://github.com/grafana/grafana/pull/112411), [@ifrost](https://github.com/ifrost) +- **Span Details:** Wrap label values [#112413](https://github.com/grafana/grafana/pull/112413), [@ifrost](https://github.com/ifrost) +- **Stars:** Refactor StarsToolbarButton and unify nav update logic [#112582](https://github.com/grafana/grafana/pull/112582), [@tomratcliffe](https://github.com/tomratcliffe) +- **Stat/BarGauge:** Border radius tweak [#112562](https://github.com/grafana/grafana/pull/112562), [@torkelo](https://github.com/torkelo) +- **Table:** Add some error-case handling to ImageCell [#110461](https://github.com/grafana/grafana/pull/110461), [@fastfrwrd](https://github.com/fastfrwrd) +- **Table:** Allow FieldType.other containing arrays to use Pills [#111205](https://github.com/grafana/grafana/pull/111205), [@fastfrwrd](https://github.com/fastfrwrd) +- **Table:** Disable virtualization, hover overflow, and scrollbar width resizing on Safari 26 [#111834](https://github.com/grafana/grafana/pull/111834), [@fastfrwrd](https://github.com/fastfrwrd) +- **Table:** Pill and JSON Cells should allow formatting [#111951](https://github.com/grafana/grafana/pull/111951), [@fastfrwrd](https://github.com/fastfrwrd) +- **Table:** Support DataLinks and Actions in SparklineCell [#112244](https://github.com/grafana/grafana/pull/112244), [@fastfrwrd](https://github.com/fastfrwrd) +- **Table:** Update ad-hoc filter to use name instead of displayName [#112815](https://github.com/grafana/grafana/pull/112815), [@fastfrwrd](https://github.com/fastfrwrd) +- **Tempo:** Migrates tags and tag values to datasource backend CallResource requests (Enterprise) +- **Theme:** Changes light theme canvas color a more white shade [#111318](https://github.com/grafana/grafana/pull/111318), [@torkelo](https://github.com/torkelo) +- **Themes:** Update themes border radius [#111478](https://github.com/grafana/grafana/pull/111478), [@torkelo](https://github.com/torkelo) +- **TimeComparison:** Automatically show/hide menu on hover [#112750](https://github.com/grafana/grafana/pull/112750), [@jesdavpet](https://github.com/jesdavpet) +- **TimeSeries:** Allow custom time units on x-axis [#112913](https://github.com/grafana/grafana/pull/112913), [@leeoniya](https://github.com/leeoniya) +- **Timeseries:** Numeric duration values could render as NaN (#73795) [#112076](https://github.com/grafana/grafana/pull/112076), [@fastfrwrd](https://github.com/fastfrwrd) +- **Transformations:** Hide "Match all/any" conditions for less than two filters [#109754](https://github.com/grafana/grafana/pull/109754), [@sudoice](https://github.com/sudoice) +- **UI Extensions:** Remove path validation from link extensions [#112259](https://github.com/grafana/grafana/pull/112259), [@leventebalogh](https://github.com/leventebalogh) + +### Bug fixes + +- **Access Control:** Fix the permission checks for saving/updating/deleting annotations [#112953](https://github.com/grafana/grafana/pull/112953), [@IevaVasiljeva](https://github.com/IevaVasiljeva) +- **Accessibility:** Improve no-unreduced-motion rule and fix violations [#110304](https://github.com/grafana/grafana/pull/110304), [@tomratcliffe](https://github.com/tomratcliffe) +- **Alerting Provisioning:** Don't error on recording rules without conditions [#109410](https://github.com/grafana/grafana/pull/109410), [@djpnicholls](https://github.com/djpnicholls) +- **Alerting:** Clear outdated settings when switching contact point type [#111869](https://github.com/grafana/grafana/pull/111869), [@konrad147](https://github.com/konrad147) +- **Alerting:** Fix enrichment tab to be rendered only for grafana alerting rules [#113030](https://github.com/grafana/grafana/pull/113030), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix instances matching in notification policies [#112326](https://github.com/grafana/grafana/pull/112326), [@konrad147](https://github.com/konrad147) +- **Alerting:** Fix threshold params [#111645](https://github.com/grafana/grafana/pull/111645), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix unmarshalling of GettableStatus to include time intervals [#112602](https://github.com/grafana/grafana/pull/112602), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Migrate `spec.title` and `spec.name` fieldSelectors [#111993](https://github.com/grafana/grafana/pull/111993), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Normalize health when filtering rules [#113087](https://github.com/grafana/grafana/pull/113087), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Prohibit receivers with empty name [#113064](https://github.com/grafana/grafana/pull/113064), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Provisioning to fix contact point type on save [#112246](https://github.com/grafana/grafana/pull/112246), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Remove \_\_grafana_origin when duplicating rule [#112396](https://github.com/grafana/grafana/pull/112396), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **AnnoList:** Fix annotations not loading when in a repeated row [#111540](https://github.com/grafana/grafana/pull/111540), [@joshhunt](https://github.com/joshhunt) +- **Annotations:** Fix issue with transformation logic in scenes [#112288](https://github.com/grafana/grafana/pull/112288), [@fastfrwrd](https://github.com/fastfrwrd) +- **Auth:** Fix render user OAuth passthrough [#111636](https://github.com/grafana/grafana/pull/111636), [@charandas](https://github.com/charandas) +- **ComboBox:** Add loading state to dropdown and prefixIcon [#112967](https://github.com/grafana/grafana/pull/112967), [@tomratcliffe](https://github.com/tomratcliffe) +- **Connections:** Fix connections home page on enterprise [#111751](https://github.com/grafana/grafana/pull/111751), [@oshirohugo](https://github.com/oshirohugo) +- **Dashboard:** Fix editor specific permissions in /api [#113292](https://github.com/grafana/grafana/pull/113292), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Dashboards:** Fix bug with anon users with editor permissions creating dashboards [#113260](https://github.com/grafana/grafana/pull/113260), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Dashboards:** Fix missing Ctrl+O keyboard shortcut for crosshair toggle [#111310](https://github.com/grafana/grafana/pull/111310), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Dashboards:** Fix moving to root folder [#111515](https://github.com/grafana/grafana/pull/111515), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Dashboards:** Fix preload field not being persisted via /v1beta1 [#112475](https://github.com/grafana/grafana/pull/112475), [@ivanortegaalba](https://github.com/ivanortegaalba) +- **Flame Graph:** Use suffix for values formatted with a short formatter [#110999](https://github.com/grafana/grafana/pull/110999), [@ifrost](https://github.com/ifrost) +- **FlameGraph:** Ensure total is only counted once for recursive function calls [#111548](https://github.com/grafana/grafana/pull/111548), [@simonswine](https://github.com/simonswine) +- **FolderPermissions:** Return 404 error when folder does not exist instead of 500 [#112919](https://github.com/grafana/grafana/pull/112919), [@Jguer](https://github.com/Jguer) +- **FolderPicker:** Fix expand toggle also selecting folder [#111755](https://github.com/grafana/grafana/pull/111755), [@aocenas](https://github.com/aocenas) +- **Graphite:** Fix legacy response unmarshalling [#112968](https://github.com/grafana/grafana/pull/112968), [@aangelisc](https://github.com/aangelisc) +- **Histogram:** Properly handle sparse heatmap-cells frames [#112907](https://github.com/grafana/grafana/pull/112907), [@leeoniya](https://github.com/leeoniya) +- **LDAP Authentication:** Fix URL to propagate username context as parameter [#111723](https://github.com/grafana/grafana/pull/111723), [@bradleypettit](https://github.com/bradleypettit) +- **Node graph:** Fix context menu position after scrolling [#112374](https://github.com/grafana/grafana/pull/112374), [@adrapereira](https://github.com/adrapereira) +- **Playlist:** Fix navigation issues with emoji-titled dashboards during dual-write migration [#111659](https://github.com/grafana/grafana/pull/111659), [@axelavargas](https://github.com/axelavargas) +- **Plugin Details Page:** Fix tabs not loading on hard refresh [#112915](https://github.com/grafana/grafana/pull/112915), [@sunker](https://github.com/sunker) +- **Plugin navigation:** Fix active nav item selection when there are more than 10 items in a group [#112886](https://github.com/grafana/grafana/pull/112886), [@aocenas](https://github.com/aocenas) +- **Plugins:** Dependencies do not inherit parent URL for preinstall [#111762](https://github.com/grafana/grafana/pull/111762), [@wbrowne](https://github.com/wbrowne) +- **Plugins:** Set isProvisioned for local plugins without remote counterpart [#111268](https://github.com/grafana/grafana/pull/111268), [@oshirohugo](https://github.com/oshirohugo) +- **Prometheus:** Fix incremental querying logic for public dashboards [#111642](https://github.com/grafana/grafana/pull/111642), [@jcolladokuri](https://github.com/jcolladokuri) +- **Prometheus:** Fix parsing logic of prometheus expressions to honor the order of binary operations [#112220](https://github.com/grafana/grafana/pull/112220), [@jcolladokuri](https://github.com/jcolladokuri) +- **Security:** fix for CVE-2025-41115 in SCIM (System for Cross-domain Identity Management) (Enterprise) +- **SoloPanel:** Fixes issue with solo route and scopes variable [#112769](https://github.com/grafana/grafana/pull/112769), [@torkelo](https://github.com/torkelo) +- **Stars:** Fix starred state not being updated [#111936](https://github.com/grafana/grafana/pull/111936), [@Clarity-89](https://github.com/Clarity-89) +- **Stat:** Fix math for percent change value heights when sparkline is not rendered [#112599](https://github.com/grafana/grafana/pull/112599), [@fastfrwrd](https://github.com/fastfrwrd) +- **StateTimeline:** Fix color display in tooltip [#112878](https://github.com/grafana/grafana/pull/112878), [@fastfrwrd](https://github.com/fastfrwrd) +- **Table:** Fix cell inspect for Sparkline and inferred JSON cells [#113059](https://github.com/grafana/grafana/pull/113059), [@fastfrwrd](https://github.com/fastfrwrd) +- **TextPanel:** Fix `CodeEditor` not appearing properly [#111937](https://github.com/grafana/grafana/pull/111937), [@ashharrison90](https://github.com/ashharrison90) +- **UnitPicker/Cascader:** Fixes type to search for unit feature [#112614](https://github.com/grafana/grafana/pull/112614), [@torkelo](https://github.com/torkelo) +- **VizTooltip:** Better overflow handling on long series names [#112240](https://github.com/grafana/grafana/pull/112240), [@fastfrwrd](https://github.com/fastfrwrd) + +### Breaking changes + +- **Faro:** Update configuration with best practices [#112108](https://github.com/grafana/grafana/pull/112108), [@joshhunt](https://github.com/joshhunt) +- **LibraryPanels:** Remove unique name constraints [#113077](https://github.com/grafana/grafana/pull/113077), [@ryantxu](https://github.com/ryantxu) +- **RBAC:** Only write action sets [#112429](https://github.com/grafana/grafana/pull/112429), [@IevaVasiljeva](https://github.com/IevaVasiljeva) + +### Plugin development fixes & changes + +- **Checkbox:** Improve accessibility of the `indeterminate` state [#112388](https://github.com/grafana/grafana/pull/112388), [@ashharrison90](https://github.com/ashharrison90) +- **Collapse:** Improve layout and deprecate `collapsible` prop [#113164](https://github.com/grafana/grafana/pull/113164), [@ashharrison90](https://github.com/ashharrison90) +- **Docs:** Add storybook links to components [#113102](https://github.com/grafana/grafana/pull/113102), [@samsch](https://github.com/samsch) +- **Modal:** Fix button focus being clipped [#112867](https://github.com/grafana/grafana/pull/112867), [@ashharrison90](https://github.com/ashharrison90) +- **Slider:** Expose prop to control visibility of input [#113084](https://github.com/grafana/grafana/pull/113084), [@ashharrison90](https://github.com/ashharrison90) +- **Slider:** Make `inputId` a required param and fix minor a11y violations [#112006](https://github.com/grafana/grafana/pull/112006), [@ashharrison90](https://github.com/ashharrison90) + + + + +# 12.1.4 (2025-11-19) + +### Bug fixes + +- **Alerting:** Fix unmarshalling of GettableStatus to include time intervals [#112733](https://github.com/grafana/grafana/pull/112733), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **AnalyticsSummaries:** Fix dashboard rollup not resetting "last X days" metrics to zero (Enterprise) +- **AnalyticsSummaries:** Fix dashboard rollup totals resetting incorrectly (Enterprise) +- **Security:** fix for CVE-2025-41115 in SCIM (System for Cross-domain Identity Management) (Enterprise) + + + + +# 12.0.7 (2025-11-19) + +### Bug fixes + +- **Alerting:** Fix unmarshalling of GettableStatus to include time intervals [#112732](https://github.com/grafana/grafana/pull/112732), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **AnalyticsSummaries:** Fix dashboard rollup not resetting "last X days" metrics to zero (Enterprise) +- **AnalyticsSummaries:** Fix dashboard rollup totals resetting incorrectly (Enterprise) +- **Security:** fix for CVE-2025-41115 in SCIM (System for Cross-domain Identity Management) (Enterprise) + + + + +# 12.2.2 (2025-11-19) + +### Features and enhancements + +- **Access control:** Reduce memory usage when fetching user's permissions [#113414](https://github.com/grafana/grafana/pull/113414), [@hairyhenderson](https://github.com/hairyhenderson) +- **Table:** Pill and JSON Cells should allow formatting [#113130](https://github.com/grafana/grafana/pull/113130), [@fastfrwrd](https://github.com/fastfrwrd) + +### Bug fixes + +- **AnalyticsSummaries:** Fix dashboard rollup not resetting "last X days" metrics to zero (Enterprise) +- **AnalyticsSummaries:** Fix dashboard rollup totals resetting incorrectly (Enterprise) +- **Security:** fix for CVE-2025-41115 in SCIM (System for Cross-domain Identity Management) (Enterprise) + + + + +# 11.6.8 (2025-11-19) + +### Bug fixes + +- **Alerting:** Fix unmarshalling of GettableStatus to include time intervals [#112731](https://github.com/grafana/grafana/pull/112731), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **AnalyticsSummaries:** Fix dashboard rollup not resetting "last X days" metrics to zero (Enterprise) +- **AnalyticsSummaries:** Fix dashboard rollup totals resetting incorrectly (Enterprise) +- **Seeder:** Add check in `filterRemovedPermissions` for already existing new permissions before seeding (Enterprise) + + + + +# 12.2.1+security-01 (2025-11-19) + +### Bug fixes + +- **Security:** fix for CVE-2025-41115 in SCIM (System for Cross-domain Identity Management) (Enterprise) + + + + +# 12.1.3+security-01 (2025-11-19) + +### Bug fixes + +- **Security:** fix for CVE-2025-41115 in SCIM (System for Cross-domain Identity Management) (Enterprise) + + + + + +# 12.0.6+security-01 (2025-11-19) + +### Bug fixes + +- **Security:** fix for CVE-2025-41115 in SCIM (System for Cross-domain Identity Management) (Enterprise) + + # 12.2.1 (2025-10-21) diff --git a/Dockerfile b/Dockerfile index 7cb65f4a05f..c07eb0360e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -98,6 +98,7 @@ COPY apps/shorturl apps/shorturl COPY apps/annotation apps/annotation COPY apps/correlations apps/correlations COPY apps/preferences apps/preferences +COPY apps/collections apps/collections COPY apps/provisioning apps/provisioning COPY apps/secret apps/secret COPY apps/scope apps/scope @@ -106,7 +107,6 @@ COPY apps/logsdrilldown apps/logsdrilldown COPY apps/advisor apps/advisor COPY apps/dashboard apps/dashboard COPY apps/folder apps/folder -COPY apps/preferences apps/preferences COPY apps/iam apps/iam COPY apps apps COPY kindsv2 kindsv2 diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 70c3e695347..9a3fabfd18c 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -6,11 +6,11 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/google/go-cmp v0.7.0 github.com/google/go-github/v70 v70.0.0 - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/grafana/grafana-plugin-sdk-go v0.282.0 + github.com/grafana/grafana-plugin-sdk-go v0.283.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 @@ -62,7 +62,7 @@ require ( github.com/apache/arrow-go/v18 v18.4.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/at-wat/mqtt-go v0.19.4 // indirect + github.com/at-wat/mqtt-go v0.19.6 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect @@ -147,7 +147,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/google/wire v0.7.0 // indirect - github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // indirect + github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect @@ -232,7 +232,7 @@ require ( github.com/prometheus/alertmanager v0.28.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index ca0ee360b74..ef7adf779d1 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -166,8 +166,8 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ= -github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA= +github.com/at-wat/mqtt-go v0.19.6 h1:+jo59yGHB3oqBU8nSFYbq6KDgN4ngMpqts5Qtyq61pc= +github.com/at-wat/mqtt-go v0.19.6/go.mod h1:Y/YrYwUlLE4N53//699Zir3WJnxSdJojvLEAbZxJjgQ= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= @@ -647,12 +647,12 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae h1:NLPwY3tIP0lg0g9wTRiMcypm6VRXW6W+MOLBsq8JSVA= -github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 h1:nbxKRtrbuhvOYmI2RhOYauHRJCtpR+vTNIgg1lFUCws= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181/go.mod h1:VtPNIFlEOJPPEc13Ax6ZTbNV3M/sAzLID72YjgzOPVA= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= @@ -667,8 +667,8 @@ github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk= github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc= -github.com/grafana/grafana-plugin-sdk-go v0.282.0 h1:9JT9E6T0h8HsK9emTZ5s01uCKaf0Ok4PjYbb7M/iOjM= -github.com/grafana/grafana-plugin-sdk-go v0.282.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s= @@ -1046,8 +1046,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 521bb0b586f..c29ea0b00a8 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -32,7 +32,6 @@ require ( github.com/go-openapi/swag/jsonname v0.25.1 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -60,7 +59,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 7a53f8de14c..b67c48676b8 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -140,8 +140,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index c262035d973..17aa567f077 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/prometheus/common v0.67.2 + github.com/prometheus/common v0.67.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index 19ac2c8b88a..988575c7ce1 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -49,7 +49,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/collections/Makefile b/apps/collections/Makefile new file mode 100644 index 00000000000..bc8d6d30cb5 --- /dev/null +++ b/apps/collections/Makefile @@ -0,0 +1,10 @@ +include ../sdk.mk + +.PHONY: generate # Run Grafana App SDK code generation +generate: install-app-sdk update-app-sdk + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --genoperatorstate=false \ + --defencoding=none \ No newline at end of file diff --git a/apps/collections/go.mod b/apps/collections/go.mod new file mode 100644 index 00000000000..281a6579c67 --- /dev/null +++ b/apps/collections/go.mod @@ -0,0 +1,69 @@ +module github.com/grafana/grafana/apps/collections + +go 1.25.3 + +require ( + github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 + github.com/stretchr/testify v1.11.1 + k8s.io/apimachinery v0.34.2 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.3 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/client-go v0.34.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/apps/collections/go.sum b/apps/collections/go.sum new file mode 100644 index 00000000000..8d576bfaa8b --- /dev/null +++ b/apps/collections/go.sum @@ -0,0 +1,187 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= +github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= +github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= +github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/collections/kinds/cue.mod/module.cue b/apps/collections/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..6280fd8f227 --- /dev/null +++ b/apps/collections/kinds/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "github.com/grafana/grafana/apps/preferences/kinds" +language: { + version: "v0.9.0" +} diff --git a/apps/collections/kinds/manifest.cue b/apps/collections/kinds/manifest.cue new file mode 100644 index 00000000000..8052da29431 --- /dev/null +++ b/apps/collections/kinds/manifest.cue @@ -0,0 +1,17 @@ +package preferences + +manifest: { + appName: "collections" + groupOverride: "collections.grafana.app" + versions: { + "v1alpha1": { + codegen: { + ts: {enabled: false} + go: {enabled: true} + } + kinds: [ + starsV1alpha1, + ] + } + } +} \ No newline at end of file diff --git a/apps/preferences/kinds/stars.cue b/apps/collections/kinds/stars.cue similarity index 100% rename from apps/preferences/kinds/stars.cue rename to apps/collections/kinds/stars.cue diff --git a/apps/collections/pkg/apis/collections/v1alpha1/constants.go b/apps/collections/pkg/apis/collections/v1alpha1/constants.go new file mode 100644 index 00000000000..4eb592236df --- /dev/null +++ b/apps/collections/pkg/apis/collections/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "collections.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/collections/pkg/apis/collections/v1alpha1/register.go b/apps/collections/pkg/apis/collections/v1alpha1/register.go new file mode 100644 index 00000000000..6b72ab94146 --- /dev/null +++ b/apps/collections/pkg/apis/collections/v1alpha1/register.go @@ -0,0 +1,58 @@ +package v1alpha1 + +import ( + "fmt" + time "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +var StarsResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion, + "stars", "stars", "Stars", + func() runtime.Object { return &Stars{} }, + func() runtime.Object { return &StarsList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]any, error) { + p, ok := obj.(*Stars) + if ok && p != nil { + return []any{ + p.Name, + p.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected stars") + }, + }, +) + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme + schemeGroupVersion = GroupVersion +) + +func init() { + localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(schemeGroupVersion, + &Stars{}, + &StarsList{}, + ) + metav1.AddToGroupVersion(scheme, schemeGroupVersion) + return nil +} + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return nil // return RegisterDefaults(scheme) +} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars.go b/apps/collections/pkg/apis/collections/v1alpha1/stars.go similarity index 100% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars.go diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_client_gen.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_client_gen.go similarity index 100% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars_client_gen.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars_client_gen.go diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_codec_gen.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_codec_gen.go similarity index 100% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars_codec_gen.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars_codec_gen.go diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_metadata_gen.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_metadata_gen.go similarity index 100% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars_metadata_gen.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars_metadata_gen.go diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_object_gen.go similarity index 100% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars_object_gen.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars_object_gen.go diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_schema_gen.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_schema_gen.go similarity index 92% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars_schema_gen.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars_schema_gen.go index 00e153b5ef2..2be9d56a29f 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_schema_gen.go +++ b/apps/collections/pkg/apis/collections/v1alpha1/stars_schema_gen.go @@ -10,7 +10,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( - schemaStars = resource.NewSimpleSchema("preferences.grafana.app", "v1alpha1", &Stars{}, &StarsList{}, resource.WithKind("Stars"), + schemaStars = resource.NewSimpleSchema("collections.grafana.app", "v1alpha1", &Stars{}, &StarsList{}, resource.WithKind("Stars"), resource.WithPlural("stars"), resource.WithScope(resource.NamespacedScope)) kindStars = resource.Kind{ Schema: schemaStars, diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_spec_gen.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_spec_gen.go similarity index 100% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars_spec_gen.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars_spec_gen.go diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/stars_test.go b/apps/collections/pkg/apis/collections/v1alpha1/stars_test.go similarity index 100% rename from apps/preferences/pkg/apis/preferences/v1alpha1/stars_test.go rename to apps/collections/pkg/apis/collections/v1alpha1/stars_test.go diff --git a/apps/collections/pkg/apis/collections/v1alpha1/zz_generated.openapi.go b/apps/collections/pkg/apis/collections/v1alpha1/zz_generated.openapi.go new file mode 100644 index 00000000000..5b486b690c0 --- /dev/null +++ b/apps/collections/pkg/apis/collections/v1alpha1/zz_generated.openapi.go @@ -0,0 +1,187 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-License-Identifier: AGPL-3.0-only + +// Code generated by openapi-gen. DO NOT EDIT. + +package v1alpha1 + +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/apps/collections/pkg/apis/collections/v1alpha1.Stars": schema_pkg_apis_collections_v1alpha1_Stars(ref), + "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsList": schema_pkg_apis_collections_v1alpha1_StarsList(ref), + "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsResource": schema_pkg_apis_collections_v1alpha1_StarsResource(ref), + "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsSpec": schema_pkg_apis_collections_v1alpha1_StarsSpec(ref), + } +} + +func schema_pkg_apis_collections_v1alpha1_Stars(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{ + Description: "Spec is the spec of the Stars", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_collections_v1alpha1_StarsList(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/apps/collections/pkg/apis/collections/v1alpha1.Stars"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.Stars", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_collections_v1alpha1_StarsResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "names": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "The set of resources", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"group", "kind", "names"}, + }, + }, + } +} + +func schema_pkg_apis_collections_v1alpha1_StarsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "resource": { + 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/apps/collections/pkg/apis/collections/v1alpha1.StarsResource"), + }, + }, + }, + }, + }, + }, + Required: []string{"resource"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsResource"}, + } +} diff --git a/apps/collections/pkg/apis/collections/v1alpha1/zz_generated.openapi_violation_exceptions.list b/apps/collections/pkg/apis/collections/v1alpha1/zz_generated.openapi_violation_exceptions.list new file mode 100644 index 00000000000..efaa822c24f --- /dev/null +++ b/apps/collections/pkg/apis/collections/v1alpha1/zz_generated.openapi_violation_exceptions.list @@ -0,0 +1,2 @@ +API rule violation: list_type_missing,github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1,StarsSpec,Resource +API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1,StarsList,ListMeta diff --git a/apps/collections/pkg/apis/collections_manifest.go b/apps/collections/pkg/apis/collections_manifest.go new file mode 100644 index 00000000000..6c3f3cfe930 --- /dev/null +++ b/apps/collections/pkg/apis/collections_manifest.go @@ -0,0 +1,132 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "encoding/json" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" +) + +var ( + rawSchemaStarsv1alpha1 = []byte(`{"Resource":{"additionalProperties":false,"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"Stars":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"resource":{"items":{"$ref":"#/components/schemas/Resource"},"type":"array"}},"required":["resource"],"type":"object"}}`) + versionSchemaStarsv1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "collections", + Group: "collections.grafana.app", + PreferredVersion: "v1alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v1alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "Stars", + Plural: "Stars", + Scope: "Namespaced", + Conversion: false, + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, + Schema: &versionSchemaStarsv1alpha1, + }, + }, + Routes: app.ManifestVersionRoutes{ + Namespaced: map[string]spec3.PathProps{}, + Cluster: map[string]spec3.PathProps{}, + Schemas: map[string]spec.Schema{}, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("collections") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "Stars/v1alpha1": v1alpha1.StarsKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 898b631516d..988f18b89d6 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -49,7 +49,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index c8315d44434..9e98fe233ec 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -4,10 +4,10 @@ go 1.25.3 require ( cuelang.org/go v0.11.1 - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/grafana/grafana-plugin-sdk-go v0.282.0 + github.com/grafana/grafana-plugin-sdk-go v0.283.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 @@ -84,7 +84,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 5c2d4405a26..b4158332679 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -81,16 +81,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-plugin-sdk-go v0.282.0 h1:9JT9E6T0h8HsK9emTZ5s01uCKaf0Ok4PjYbb7M/iOjM= -github.com/grafana/grafana-plugin-sdk-go v0.282.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -202,8 +202,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json index 277a8b2ebc2..5091df59fcf 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.complete.json @@ -1,6 +1,6 @@ { - "apiVersion": "dashboard.grafana.app/v2alpha1", "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", "metadata": { "name": "test-v2alpha1-complete", "labels": { @@ -11,26 +11,14 @@ } }, "spec": { - "title": "Test: Complete V2alpha1 Dashboard Example", - "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", - "editable": true, - "liveNow": true, - "preload": true, - "cursorSync": "Tooltip", - "tags": ["test", "example", "migration"], "annotations": [ { "kind": "AnnotationQuery", "spec": { - "builtIn": true, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, - "enable": true, - "hide": false, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", "query": { "kind": "grafana", "spec": { @@ -39,257 +27,39 @@ "tags": [], "type": "dashboard" } - } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true } }, { "kind": "AnnotationQuery", "spec": { - "builtIn": false, "datasource": { "type": "prometheus", "uid": "gdev-prometheus" }, - "enable": true, - "hide": false, - "iconColor": "yellow", - "name": "Prometheus Annotations", "query": { "kind": "prometheus", "spec": { "expr": "changes(process_start_time_seconds[1m])", "refId": "Anno" } - } - } - } - ], - "variables": [ - { - "kind": "QueryVariable", - "spec": { - "name": "prometheus_query", - "datasource": { - "type": "prometheus", - "uid": "gdev-prometheus" }, - "query": { - "kind": "prometheus", - "spec": { - "expr": "up" - } - }, - "refresh": "time", - "regex": "", - "sort": "alphabetical", - "multi": true, - "includeAll": true, - "current": { - "text": "All", - "value": ["$__all"] - }, - "hide": "dontHide", - "label": "Prometheus Query", - "description": "Shows all up metrics", - "skipUrlSync": false, - "definition": "up" - } - }, - { - "kind": "TextVariable", - "spec": { - "name": "text_var", - "label": "Text Variable", - "description": "A simple text variable", - "query": "server1,server2,server3", - "current": { - "selected": true, - "text": "server1", - "value": "server1" - }, - "hide": "dontHide", - "skipUrlSync": false - } - }, - { - "kind": "ConstantVariable", - "spec": { - "name": "constant_var", - "label": "Constant", - "description": "A constant value", - "query": "production", - "current": { - "selected": true, - "text": "production", - "value": "production" - }, - "hide": "dontHide", - "skipUrlSync": true - } - }, - { - "kind": "DatasourceVariable", - "spec": { - "name": "ds_var", - "label": "Datasource", - "description": "Select a datasource", - "pluginId": "prometheus", - "refresh": "load", - "regex": "/^gdev-/", - "current": { - "text": "gdev-prometheus", - "value": "gdev-prometheus" - }, - "options": [ - { - "text": "gdev-prometheus", - "value": "gdev-prometheus" - } - ], - "multi": false, - "includeAll": false, - "hide": "dontHide", - "skipUrlSync": false - } - }, - { - "kind": "IntervalVariable", - "spec": { - "name": "interval", - "label": "Interval", - "description": "Time interval selection", - "query": "1m,5m,10m,30m,1h,6h,12h,1d", - "current": { - "selected": true, - "text": "5m", - "value": "5m" - }, - "options": [ - { - "text": "1m", - "value": "1m" - }, - { - "text": "5m", - "value": "5m" - }, - { - "text": "10m", - "value": "10m" - }, - { - "text": "30m", - "value": "30m" - }, - { - "text": "1h", - "value": "1h" - }, - { - "text": "6h", - "value": "6h" - }, - { - "text": "12h", - "value": "12h" - }, - { - "text": "1d", - "value": "1d" - } - ], - "auto": true, - "auto_min": "10s", - "auto_count": 30, - "refresh": "load", - "hide": "dontHide", - "skipUrlSync": false - } - }, - { - "kind": "CustomVariable", - "spec": { - "name": "custom_var", - "label": "Custom Options", - "description": "Custom multi-value variable", - "query": "prod : Production, staging : Staging, dev : Development", - "current": { - "text": ["Production"], - "value": ["prod"] - }, - "options": [ - { - "text": "Production", - "value": "prod" - }, - { - "text": "Staging", - "value": "staging" - }, - { - "text": "Development", - "value": "dev" - } - ], - "multi": true, - "includeAll": true, - "allValue": "*", - "hide": "dontHide", - "skipUrlSync": false, - "allowCustomValue": true - } - }, - { - "kind": "GroupByVariable", - "spec": { - "name": "group_by", - "datasource": { - "type": "prometheus", - "uid": "gdev-prometheus" - }, - "current": { - "text": "instance", - "value": "instance" - }, - "description": "Group metrics by label", - "hide": "dontHide", - "label": "Group By" - } - }, - { - "kind": "AdhocVariable", - "spec": { - "name": "filters", - "datasource": { - "type": "prometheus", - "uid": "gdev-prometheus" - }, - "baseFilters": [ - { - "key": "job", - "operator": "=", - "value": "grafana", - "condition": "AND" - } - ], - "filters": [], - "hide": "dontHide", - "label": "Filters", - "defaultKeys": [ - { - "text": "job", - "value": "job", - "expandable": true - }, - { - "text": "instance", - "value": "instance", - "expandable": true - } - ] + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false } } ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, "elements": { "panel-1": { "kind": "Panel", @@ -297,6 +67,7 @@ "id": 1, "title": "Panel with Conditional Rendering", "description": "This panel demonstrates conditional rendering features", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -304,16 +75,18 @@ { "kind": "PanelQuery", "spec": { - "refId": "A", - "datasource": { - "uid": "gdev-prometheus" - }, "query": { "kind": "prometheus", "spec": { "expr": "up{job=\"grafana\"}" } - } + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false } } ], @@ -323,13 +96,16 @@ "spec": { "id": "reduce", "options": { - "mode": "reduceFields", "includeTimeField": false, - "reducers": ["mean"] + "mode": "reduceFields", + "reducers": [ + "mean" + ] } } } - ] + ], + "queryOptions": {} } }, "vizConfig": { @@ -344,22 +120,6 @@ }, "fieldConfig": { "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": 0 - }, - { - "color": "green", - "value": 1 - } - ] - }, "mappings": [ { "type": "value", @@ -374,7 +134,23 @@ } } } - ] + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } }, "overrides": [] } @@ -448,15 +224,281 @@ ] } }, + "links": [], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], "timeSettings": { "timezone": "browser", - "weekStart": "monday", - "fiscalYearStartMonth": 0, - "autoRefresh": "10s", - "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], "from": "now-6h", "to": "now", - "hideTimepicker": false + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "load", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "spec": { + "name": "group_by", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "filters", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json index 408a3cc9873..a27c33c3239 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.ds-data-query.json @@ -1,25 +1,18 @@ { - "apiVersion": "dashboard.grafana.app/v2alpha1", "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", "metadata": { - "name": "test-v2alpha1-annotations", - "labels": {}, - "annotations": {} + "name": "test-v2alpha1-annotations" }, "spec": { "annotations": [ { "kind": "AnnotationQuery", "spec": { - "builtIn": true, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, - "enable": true, - "hide": false, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", "query": { "kind": "grafana", "spec": { @@ -28,21 +21,21 @@ "tags": [], "type": "dashboard" } - } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true } }, { "kind": "AnnotationQuery", "spec": { - "builtIn": false, "datasource": { "type": "grafana-testdata-datasource", "uid": "PD8C576611E62080A" }, - "enable": true, - "hide": false, - "iconColor": "blue", - "name": "testdata-annos", "query": { "kind": "grafana-testdata-datasource", "spec": { @@ -50,17 +43,17 @@ "refId": "Anno", "scenarioId": "annotations" } - } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false } }, { "kind": "AnnotationQuery", "spec": { - "builtIn": false, - "enable": true, - "hide": false, - "iconColor": "blue", - "name": "no-ds-testdata-annos", "query": { "kind": "prometheus", "spec": { @@ -68,21 +61,41 @@ "refId": "Anno", "scenarioId": "annotations" } - } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false } }, { "kind": "AnnotationQuery", "spec": { - "builtIn": false, "datasource": { "type": "prometheus", "uid": "gdev-prometheus" }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, "enable": true, "hide": false, "iconColor": "yellow", "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { "query": { "kind": "prometheus", "spec": { @@ -92,41 +105,21 @@ "refId": "Anno", "scenarioId": "annotations" } - } - } - }, - { - "kind": "AnnotationQuery", - "spec": { - "builtIn": false, + }, "enable": true, "hide": false, "iconColor": "yellow", "name": "no-ds-prom-annos", - "query": { - "kind": "prometheus", - "spec": { - "expr": "{action=\"add_client\"}", - "interval": "", - "lines": 10, - "refId": "Anno", - "scenarioId": "annotations" - } - } + "builtIn": false } }, { "kind": "AnnotationQuery", "spec": { - "builtIn": false, "datasource": { "type": "grafana-postgresql-datasource", "uid": "PBBCEC2D313BC06C3" }, - "enable": true, - "hide": false, - "iconColor": "red", - "name": "postgress-annos", "query": { "kind": "grafana-postgresql-datasource", "spec": { @@ -154,27 +147,21 @@ "limit": 50 } } - } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false } }, { "kind": "AnnotationQuery", "spec": { - "builtIn": false, "datasource": { "type": "elasticsearch", "uid": "gdev-elasticsearch" }, - "enable": true, - "hide": false, - "iconColor": "red", - "legacyOptions": { - "tagsField": "asd", - "textField": "asd", - "timeEndField": "asdas", - "timeField": "asd" - }, - "name": "elastic - annos", "query": { "kind": "elasticsearch", "spec": { @@ -183,6 +170,17 @@ "refId": "Anno", "scenarioId": "annotations" } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" } } } @@ -193,6 +191,10 @@ "panel-1": { "kind": "Panel", "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -200,35 +202,58 @@ { "kind": "PanelQuery", "spec": { - "hidden": false, - "datasource": { - "uid": "gdev-testdata" - }, "query": { "kind": "grafana-testdata-datasource", "spec": { "scenarioId": "random_walk", "seriesCount": 3 - }, - "version": "v0" + } }, - "refId": "A" + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false } } ], - "queryOptions": {}, - "transformations": [] + "transformations": [], + "queryOptions": {} } }, - "description": "", - "id": 1, - "links": [], - "title": "Simple timeseries (WITH DS REF)", "vizConfig": { "kind": "timeseries", "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, "fieldConfig": { "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, "color": { "mode": "palette-classic" }, @@ -264,37 +289,10 @@ "thresholdsStyle": { "mode": "off" } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] } }, "overrides": [] - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.1.0-pre" + } } } } @@ -302,6 +300,10 @@ "panel-2": { "kind": "Panel", "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -309,51 +311,26 @@ { "kind": "PanelQuery", "spec": { - "hidden": false, "query": { "kind": "grafana-testdata-datasource", "spec": { "scenarioId": "random_walk", "seriesCount": 4 - }, - "version": "v0" + } }, - "refId": "A" + "refId": "A", + "hidden": false } } ], - "queryOptions": {}, - "transformations": [] + "transformations": [], + "queryOptions": {} } }, - "description": "", - "id": 2, - "links": [], - "title": "Simple stat (NO DS REF)", "vizConfig": { "kind": "stat", "spec": { - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, + "pluginVersion": "12.1.0-pre", "options": { "colorMode": "value", "graphMode": "area", @@ -361,7 +338,9 @@ "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { - "calcs": ["lastNotNull"], + "calcs": [ + "lastNotNull" + ], "fields": "", "values": false }, @@ -369,7 +348,27 @@ "textMode": "auto", "wideLayout": true }, - "pluginVersion": "12.1.0-pre" + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } } } } @@ -377,6 +376,10 @@ "panel-3": { "kind": "Panel", "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -384,7 +387,6 @@ { "kind": "PanelQuery", "spec": { - "hidden": false, "query": { "kind": "prometheus", "spec": { @@ -397,26 +399,49 @@ "legendFormat": "__auto", "range": true, "useBackend": false - }, - "version": "v0" + } }, - "refId": "A" + "refId": "A", + "hidden": false } } ], - "queryOptions": {}, - "transformations": [] + "transformations": [], + "queryOptions": {} } }, - "description": "", - "id": 3, - "links": [], - "title": "Panel with NO REF to gdev-prometheus", "vizConfig": { "kind": "timeseries", "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, "fieldConfig": { "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, "color": { "mode": "palette-classic" }, @@ -452,37 +477,10 @@ "thresholdsStyle": { "mode": "off" } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] } }, "overrides": [] - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.1.0-pre" + } } } } @@ -490,6 +488,10 @@ "panel-4": { "kind": "Panel", "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -497,10 +499,6 @@ { "kind": "PanelQuery", "spec": { - "hidden": false, - "datasource": { - "uid": "gdev-prometheus" - }, "query": { "kind": "prometheus", "spec": { @@ -513,26 +511,53 @@ "legendFormat": "__auto", "range": true, "useBackend": false - }, - "version": "v0" + } }, - "refId": "A" + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false } } ], - "queryOptions": {}, - "transformations": [] + "transformations": [], + "queryOptions": {} } }, - "description": "", - "id": 4, - "links": [], - "title": "Panel with ref to gdev-prometheus", "vizConfig": { "kind": "timeseries", "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, "fieldConfig": { "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, "color": { "mode": "palette-classic" }, @@ -568,37 +593,10 @@ "thresholdsStyle": { "mode": "off" } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] } }, "overrides": [] - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.1.0-pre" + } } } } @@ -606,6 +604,10 @@ "panel-5": { "kind": "Panel", "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -613,10 +615,6 @@ { "kind": "PanelQuery", "spec": { - "hidden": false, - "datasource": { - "uid": "gdev-prometheus" - }, "query": { "kind": "prometheus", "spec": { @@ -628,41 +626,68 @@ "legendFormat": "__auto", "range": true, "useBackend": false - }, - "version": "v0" + } }, - "refId": "A" + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false } }, { "kind": "PanelQuery", "spec": { - "hidden": false, - "datasource": { - "uid": "gdev-testdata" - }, "query": { "kind": "grafana-testdata-datasource", - "spec": {}, - "version": "v0" + "spec": {} }, - "refId": "B" + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "B", + "hidden": false } } ], - "queryOptions": {}, - "transformations": [] + "transformations": [], + "queryOptions": {} } }, - "description": "", - "id": 5, - "links": [], - "title": "Mixed DS WITH REFS", "vizConfig": { "kind": "timeseries", "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, "fieldConfig": { "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, "color": { "mode": "palette-classic" }, @@ -698,37 +723,10 @@ "thresholdsStyle": { "mode": "off" } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] } }, "overrides": [] - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.1.0-pre" + } } } } @@ -736,6 +734,10 @@ "panel-6": { "kind": "Panel", "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -743,7 +745,6 @@ { "kind": "PanelQuery", "spec": { - "hidden": false, "query": { "kind": "prometheus", "spec": { @@ -755,38 +756,60 @@ "legendFormat": "__auto", "range": true, "useBackend": false - }, - "version": "v0" + } }, - "refId": "A" + "refId": "A", + "hidden": false } }, { "kind": "PanelQuery", "spec": { - "hidden": false, "query": { "kind": "grafana-testdata-datasource", - "spec": {}, - "version": "v0" + "spec": {} }, - "refId": "B" + "refId": "B", + "hidden": false } } ], - "queryOptions": {}, - "transformations": [] + "transformations": [], + "queryOptions": {} } }, - "description": "", - "id": 6, - "links": [], - "title": "Mixed DS WITHOUT REFS", "vizConfig": { "kind": "timeseries", "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, "fieldConfig": { "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, "color": { "mode": "palette-classic" }, @@ -822,37 +845,10 @@ "thresholdsStyle": { "mode": "off" } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] } }, "overrides": [] - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "12.1.0-pre" + } } } } @@ -861,7 +857,9 @@ "layout": { "kind": "AutoGridLayout", "spec": { + "maxColumnCount": 3, "columnWidthMode": "standard", + "rowHeightMode": "standard", "items": [ { "kind": "AutoGridLayoutItem", @@ -917,9 +915,7 @@ } } } - ], - "maxColumnCount": 3, - "rowHeightMode": "standard" + ] } }, "links": [], @@ -927,75 +923,136 @@ "preload": false, "tags": [], "timeSettings": { - "autoRefresh": "", - "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "fiscalYearStartMonth": 0, - "from": "now-6h", - "hideTimepicker": false, "timezone": "browser", - "to": "now" + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 }, "title": "Test: V2alpha1 dashboard with annotations", "variables": [ { "kind": "QueryVariable", "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, - "enable": true, - "hide": "dontHide", - "name": "variable-ds-prometheus", "query": { "kind": "prometheus", "spec": { "expr": "up" } - } - } - }, - { - "kind": "QueryVariable", - "spec": { - "enable": true, - "hide": "dontHide", - "name": "variable-no-ds", - "query": { - "kind": "grafana-testdata-datasource", - "spec": { - "scenarioId": "csv_metric_values", - "csv": "1,2,3,4" - } - } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false } }, { "kind": "QueryVariable", "spec": { - "enable": true, + "name": "variable-no-ds", + "current": { + "text": null, + "value": null + }, "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { "name": "variable-no-ds-empty-query", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, "query": { "kind": "grafana-testdata-datasource", "spec": {} - } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false } }, { "kind": "QueryVariable", "spec": { - "enable": true, - "hide": "dontHide", "name": "variable-no-default-ds", + "current": { + "text": null, + "value": null + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, "query": { "kind": "prometheus", "spec": { "expr": "up" } - } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false } } ] }, - "status": {} -} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json index 65c2b91ae72..5cb430e9655 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.groupby-adhoc-vars.json @@ -1,73 +1,98 @@ { - "apiVersion": "dashboard.grafana.app/v2alpha1", "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", "metadata": { "name": "test-v2alpha1-groupby-adhoc-vars" }, "spec": { + "annotations": [], + "cursorSync": "", + "elements": {}, + "layout": null, + "links": [], + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, "title": "Test: V2alpha1 dashboard with group by and adhoc variables", "variables": [ { "kind": "GroupByVariable", "spec": { - "current": { - "text": "text7", - "value": "value7" - }, + "name": "", "datasource": { "type": "prometheus", "uid": "gdev-prometheus" }, - "description": "A group by variable", + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", "hide": "dontHide", - "label": "Group By Variable" + "skipUrlSync": false, + "description": "A group by variable" } }, { "kind": "AdhocVariable", "spec": { - "baseFilters": [ - { - "condition": "AND", - "key": "key1", - "operator": "=", - "value": "value1" - }, - { - "condition": "OR", - "key": "key2", - "operator": "=", - "value": "value2" - } - ], + "name": "adhocVar", "datasource": { "type": "prometheus", "uid": "datasource-3" }, - "defaultKeys": [ + "baseFilters": [ { - "expandable": true, - "group": "defaultGroup1", - "text": "defaultKey1", - "value": "defaultKey1" + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" } ], - "description": "An adhoc variable", "filters": [ { - "condition": "AND", "key": "key3", "operator": "=", - "value": "value3" + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true } ], - "hide": "dontHide", "label": "Adhoc Variable", - "name": "adhocVar", + "hide": "dontHide", "skipUrlSync": false, + "description": "An adhoc variable", "allowCustomValue": true } } ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } } } \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json index 5d591e64ca0..48c2aab46af 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2alpha1.viz-config.json @@ -1,202 +1,218 @@ { - "apiVersion": "dashboard.grafana.app/v2alpha1", - "kind": "Dashboard", - "metadata": { - "name": "test-v2alpha1-viz-config", - "labels": {}, - "annotations": {} - }, - "spec": { - "elements": { - "panel-1": { - "kind": "Panel", - "spec": { - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "hidden": false, - "datasource": { - "uid": "gdev-testdata" - }, - "query": { - "kind": "grafana-testdata-datasource", - "spec": { - "scenarioId": "random_walk", - "seriesCount": 3 - }, - "version": "v0" - }, - "refId": "A" - } - } - ], - "queryOptions": {}, - "transformations": [] - } - }, - "description": "", - "id": 1, - "links": [], - "title": "Simple timeseries (WITH DS REF)", - "vizConfig": { - "kind": "timeseries", - "spec": { - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "barWidthFactor": 0.6, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 } }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": 0 - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] }, - "tooltip": { - "hideZeros": false, - "mode": "single", - "sort": "none" + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } } }, - "pluginVersion": "12.1.0-pre" + "overrides": [] } } } } - }, - "layout": { - "kind": "AutoGridLayout", - "spec": { - "columnWidthMode": "standard", - "items": [ - { - "kind": "AutoGridLayoutItem", - "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-3" - } - } - }, - { - "kind": "AutoGridLayoutItem", - "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-4" - } - } - }, - { - "kind": "AutoGridLayoutItem", - "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-2" - } - } - }, - { - "kind": "AutoGridLayoutItem", - "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-1" - } - } - }, - { - "kind": "AutoGridLayoutItem", - "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-5" - } - } - }, - { - "kind": "AutoGridLayoutItem", - "spec": { - "element": { - "kind": "ElementReference", - "name": "panel-6" - } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" } } - ], - "maxColumnCount": 3, - "rowHeightMode": "standard" - } - }, - "links": [], - "liveNow": false, - "preload": false, - "tags": [], - "timeSettings": { - "autoRefresh": "", - "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], - "fiscalYearStartMonth": 0, - "from": "now-6h", - "hideTimepicker": false, - "timezone": "browser", - "to": "now" - }, - "title": "Test: V2alpha1 dashboard with viz config" + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } }, - "status": {} + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with viz config", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } } - \ No newline at end of file +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.complete.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.complete.json new file mode 100644 index 00000000000..fda30922928 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.complete.json @@ -0,0 +1,509 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up{job=\"grafana\"}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "onTimeRangeChanged", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "group_by", + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "filters", + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.ds-data-query.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.ds-data-query.json new file mode 100644 index 00000000000..09e35c64258 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.ds-data-query.json @@ -0,0 +1,1092 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-postgresql-datasource", + "version": "v0", + "datasource": { + "name": "PBBCEC2D313BC06C3" + }, + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "gdev-elasticsearch" + }, + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds-empty-query", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-default-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.groupby-adhoc-vars.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.groupby-adhoc-vars.json new file mode 100644 index 00000000000..6f377850074 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.groupby-adhoc-vars.json @@ -0,0 +1,98 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": {}, + "layout": null, + "links": [], + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "", + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A group by variable" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "datasource-3" + }, + "spec": { + "name": "adhocVar", + "baseFilters": [ + { + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" + } + ], + "filters": [ + { + "key": "key3", + "operator": "=", + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true + } + ], + "label": "Adhoc Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "An adhoc variable", + "allowCustomValue": true + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.viz-config.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.viz-config.json new file mode 100644 index 00000000000..a7f48d67563 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v2beta1.viz-config.json @@ -0,0 +1,220 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with viz config", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2alpha1.json index 2640aa14f7b..7a4a4d2c4b7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v10.table_thresholds.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2alpha1.json index 31afdc108a2..f427b5dbcb7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -191,7 +191,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(server)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json index 053f7e18d9e..8e59bbd21ca 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v11.no-op-migration.v42.v2beta1.json @@ -197,7 +197,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(server)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2alpha1.json index 252a81e9796..f55769f82b2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -75,7 +75,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -98,7 +98,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -121,7 +121,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -144,7 +144,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -167,7 +167,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -190,7 +190,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json index 82c17382c32..febee713d97 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v12.template-variables.v42.v2beta1.json @@ -77,7 +77,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -102,7 +102,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -127,7 +127,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -152,7 +152,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -177,7 +177,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -202,7 +202,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2alpha1.json index 49108c4b128..a1ef92d3d50 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.graph_thresholds.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -178,7 +178,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -222,7 +222,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2alpha1.json index c0ab55dcacf..3edc16edae6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v13.minimal_graph_config.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "editorMode": "builder", "expr": "{\"a.utf8.metric 🤘\", job=\"prometheus-utf8\"}", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2alpha1.json index 50d3e9aff2f..e4a62f00bc7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "cpu_usage" } @@ -92,7 +92,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "memory_usage" } @@ -195,7 +195,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(server)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json index 8ee98acf062..975fc24c969 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v14.shared_crosshair_to_graph_tooltip.v42.v2beta1.json @@ -201,7 +201,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(server)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2alpha1.json index 5add17319b1..decdff12902 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -31,7 +31,7 @@ "kind": "AnnotationQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": {} }, "enable": true, @@ -66,7 +66,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "sum by (job, version) (up{job=~\".*\"})", "format": "table", @@ -209,7 +209,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", "format": "table", @@ -331,7 +331,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", "format": "table", @@ -453,7 +453,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "max by (pod) (up{job=\"mimir-aggregator\"})", "format": "time_series", @@ -541,7 +541,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "sum by (deployment) (up{job=\"kube-state-metrics\"})", "format": "table", @@ -663,7 +663,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "sum by (statefulset) (up{job=\"kube-state-metrics\"})", "format": "table", @@ -962,7 +962,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up, job)" } @@ -989,7 +989,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json index 709c7f5f422..9db5193d5af 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.mimir_rollout_debugging.v42.v2beta1.json @@ -978,7 +978,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(up, job)" @@ -1007,7 +1007,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(up{job=~\"$cluster\"}, instance)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v0alpha1.json index 2b5625047da..a6de319cc04 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v0alpha1.json @@ -95,7 +95,9 @@ "templating": { "list": [ { - "datasource": "prometheus", + "datasource": { + "type": "prometheus" + }, "name": "server", "options": [], "query": "label_values(server)", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2alpha1.json index 34d2ad77891..347556eb45d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -35,7 +35,7 @@ "uid": "grafana" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -63,7 +63,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -107,7 +107,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -208,7 +208,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(server)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json index a2a031d5fa0..756f4a76074 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v15.no-op-migration.v42.v2beta1.json @@ -215,7 +215,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(server)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2alpha1.json index f94cd8566b2..1bf45848829 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.empty-rows-and-panels-array.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -47,7 +47,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"primary\"}[$__rate_interval])", "format": "time_series", @@ -152,7 +152,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "increase(demo_latency_iota{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\", type=\"secondary\"}[$__rate_interval])", "format": "time_series", @@ -257,7 +257,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "increase(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\", service=~\"$service\"}[$__rate_interval])", "format": "time_series", @@ -366,7 +366,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "random_metric_alpha{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", "format": "time_series", @@ -475,7 +475,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "rate(sample_counter_beta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", "format": "time_series", @@ -584,7 +584,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "increase(test_errors_gamma{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", "format": "time_series", @@ -693,7 +693,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "rate(demo_events_delta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}[$__rate_interval])", "format": "time_series", @@ -802,7 +802,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "example_usage_epsilon{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", "format": "time_series", @@ -910,7 +910,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", "format": "time_series", @@ -930,7 +930,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "sample_target_eta{env=~\"$env\", region=~\"$region\", node=~\"$node\"}", "format": "time_series", @@ -1038,7 +1038,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "100 * random_total_zeta{env=~\"$env\", region=~\"$region\", node=~\"$node\"} / clamp_min(test_available_theta{env=~\"$env\", region=~\"$region\", node=~\"$node\"},1)", "format": "time_series", @@ -1346,7 +1346,7 @@ "uid": "${example_datasource}" }, "query": { - "kind": "DataQuery", + "kind": "", "spec": { "__legacyStringValue": "label_values(system_info_lambda{}, env)" } @@ -1377,7 +1377,7 @@ "uid": "${example_datasource}" }, "query": { - "kind": "DataQuery", + "kind": "", "spec": { "__legacyStringValue": "label_values(system_info_lambda{env=~\"$env\"}, region)" } @@ -1408,7 +1408,7 @@ "uid": "${example_datasource}" }, "query": { - "kind": "DataQuery", + "kind": "", "spec": { "__legacyStringValue": "label_values(system_info_lambda{env=~\"$env\", region=~\"$region\"}, node)" } @@ -1439,7 +1439,7 @@ "uid": "${example_datasource}" }, "query": { - "kind": "DataQuery", + "kind": "", "spec": { "__legacyStringValue": "label_values(example_events_kappa{env=~\"$env\", region=~\"$region\", node=~\"$node\"}, service)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2alpha1.json index c17e76fbc77..6cf8402f377 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.grid_layout_upgrade.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -178,7 +178,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -306,7 +306,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -350,7 +350,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -394,7 +394,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -438,7 +438,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2alpha1.json index c8699b49e46..221b01ffec5 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v16.span_zero_demo.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -93,7 +93,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt" } @@ -144,7 +144,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -191,7 +191,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "max(app_worker_threads_active{cluster=\"$cluster\", namespace=\"default\"})", "instant": true @@ -238,7 +238,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "tempo", "spec": { "filters": [ { @@ -305,7 +305,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}" @@ -323,7 +323,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.9, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}" @@ -341,7 +341,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}" @@ -359,7 +359,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le, resources_changed_bucket, action)) and on(resources_changed_bucket, action) sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (resources_changed_bucket, action) \u003e 0", "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}" @@ -461,7 +461,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.99, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", "legendFormat": "{{action}} q0.99 - size {{resources_changed_bucket}}" @@ -479,7 +479,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.95, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", "legendFormat": "{{action}} q0.95 - size {{resources_changed_bucket}}" @@ -497,7 +497,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.5, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", "legendFormat": "{{action}} q0.5 - size {{resources_changed_bucket}}" @@ -515,7 +515,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.1, sum(rate(app_operation_duration_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[5m])) by (le, resources_changed_bucket, action))", "legendFormat": "{{action}} q0.1 - size {{resources_changed_bucket}}" @@ -562,7 +562,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "clamp_min(sum(app_operation_queue_size{cluster=\"$cluster\", namespace=\"default\"}), 0)", "legendFormat": "Queue size" @@ -609,7 +609,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "avg(histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[7d])) by (le)))", "legendFormat": "Queue size" @@ -660,7 +660,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.99, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", "legendFormat": "q0.99" @@ -678,7 +678,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.95, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", "legendFormat": "q0.95" @@ -696,7 +696,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.5, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", "legendFormat": "q0.5" @@ -714,7 +714,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "histogram_quantile(0.1, sum(rate(app_operation_queue_wait_seconds_bucket{cluster=\"$cluster\", namespace=\"default\"}[$__rate_interval])) by (le))", "legendFormat": "q0.1" @@ -761,7 +761,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -808,7 +808,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "count by (cluster, channel)(label_replace(label_replace(kube_pod_container_info{namespace=\"default\", container=\"app-worker\", pod=~\"app-worker.*\", cluster=~\"$cluster\"}, \"version\", \"$1\", \"image\", \".+:(.+)\"), \"channel\", \"$1\", \"container\", \".+-(.+)\"))", "legendFormat": "{{cluster}}" @@ -855,7 +855,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", "legendFormat": "Memory Request" @@ -873,7 +873,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", resource=\"memory\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"})", "legendFormat": "Memory Limit" @@ -891,7 +891,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "max(container_memory_usage_bytes{namespace=\"default\",cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker.*\"}) by (pod)", "legendFormat": "Container usage {{pod}}" @@ -938,7 +938,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "sum(irate(container_cpu_usage_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container, cpu)", "legendFormat": "Usage {{pod}}" @@ -956,7 +956,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "sum(irate(container_cpu_cfs_throttled_seconds_total{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\"}[$__rate_interval])) by (pod, container)", "legendFormat": "Throttling {{pod}}" @@ -974,7 +974,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "max(kube_pod_container_resource_limits{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", "legendFormat": "CPU limit" @@ -992,7 +992,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "max(kube_pod_container_resource_requests{namespace=\"default\", cluster=~\"$cluster\", container=\"app-worker\", pod=~\"app-worker-.*\", resource=\"cpu\"})", "legendFormat": "CPU request" @@ -1039,7 +1039,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -1086,7 +1086,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -1133,7 +1133,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "sum by (action) (app_jobs_processed_total{outcome=\"success\", cluster=\"$cluster\", namespace=\"default\"})\n/\nsum by (action) (app_jobs_processed_total{cluster=\"$cluster\", namespace=\"default\"})\n", "legendFormat": "{{action}}" @@ -1199,7 +1199,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "expr": "{namespace=\"default\", cluster=\"$cluster\", job=\"app-service\"} | logfmt | level=\"error\"" } @@ -1640,7 +1640,7 @@ "uid": "${prom}" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(app_worker_threads_active,cluster)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2alpha1.json index 9534b61db24..aa9017f0720 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v17.minspan_to_maxperrow.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -178,7 +178,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -222,7 +222,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -266,7 +266,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -310,7 +310,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -354,7 +354,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -398,7 +398,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -442,7 +442,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2alpha1.json index 7c22f7d6a45..f154e2e2679 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v18.gauge_options.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -112,7 +112,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -161,7 +161,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -211,7 +211,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -267,7 +267,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2alpha1.json index 42f9b90a138..e51fc93636a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v19.panel_links.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -51,7 +51,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -100,7 +100,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -149,7 +149,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -199,7 +199,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -248,7 +248,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -292,7 +292,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2alpha1.json index 2be443726d3..129ba30c694 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "target": "cpu.usage" } @@ -92,7 +92,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "target": "memory.usage" } @@ -138,7 +138,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -182,7 +182,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "target": "disk.io" } @@ -311,7 +311,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(server)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json index 6a82b1603f8..0c80ae34850 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v2.panels-and-services.v42.v2beta1.json @@ -321,7 +321,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(server)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2alpha1.json index 81ccea2ab37..95d3f1d8834 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v20.variable_syntax_links.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -103,7 +103,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -165,7 +165,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -229,7 +229,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -293,7 +293,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2alpha1.json index 32132d368c5..0b2251f0dca 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v21.data_links_series_to_field.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -101,7 +101,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -160,7 +160,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -221,7 +221,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -272,7 +272,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2alpha1.json index fbb8429153b..dd03601b0ac 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v22.table_panel_align.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2alpha1.json index 7b4fe20e68e..a40797444f4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "up" } @@ -140,7 +140,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -169,7 +169,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -192,7 +192,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -215,7 +215,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -238,7 +238,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -261,7 +261,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -284,7 +284,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json index 8004109e441..a97d9a51e2f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v23.multi_variable_alignment.v42.v2beta1.json @@ -144,7 +144,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -175,7 +175,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -200,7 +200,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -225,7 +225,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -250,7 +250,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -275,7 +275,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -300,7 +300,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2alpha1.json index 6226b6c15e8..c9dd5bffa51 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v24.table-angular.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -61,7 +61,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -105,7 +105,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -149,7 +149,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -193,7 +193,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -237,7 +237,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -281,7 +281,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -325,7 +325,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -369,7 +369,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -413,7 +413,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -457,7 +457,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -501,7 +501,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -545,7 +545,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -589,7 +589,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -633,7 +633,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -677,7 +677,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -736,7 +736,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -780,7 +780,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2alpha1.json index e83721eca1c..3375a6bffe4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -31,7 +31,7 @@ "kind": "AnnotationQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": {} }, "enable": true, @@ -59,7 +59,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -103,7 +103,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -204,7 +204,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json index 7266cb6084c..b873fdea3a3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v25.no-op-migration.v42.v2beta1.json @@ -212,7 +212,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2alpha1.json index 001fe0cf137..a135614baba 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v26.text2_to_text.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2alpha1.json index c722d627187..51cdcba85c1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v27.repeated_panels_and_constant_variable.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2alpha1.json index dd56c1cd995..54384f8dc06 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -75,7 +75,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up, instance)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json index c938e4a730f..6626b919137 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.remove_variable_properties.v42.v2beta1.json @@ -77,7 +77,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(up, instance)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2alpha1.json index c819be43bc1..82fc46be585 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -61,7 +61,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -105,7 +105,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -149,7 +149,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -164,7 +164,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -208,7 +208,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "rate(http_requests_total[5m])" } @@ -337,7 +337,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up, instance)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json index 5a0d8b3a96b..4eff500bef6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_and_variable_properties.v42.v2beta1.json @@ -349,7 +349,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(up, instance)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2alpha1.json index 985b316822d..0873ad2190c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v28.singlestat_migration.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -61,7 +61,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -105,7 +105,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -120,7 +120,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -164,7 +164,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -208,7 +208,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -223,7 +223,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -267,7 +267,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -313,7 +313,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -400,7 +400,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -452,7 +452,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "rate(http_requests_total[5m])" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2alpha1.json index ce4f5103c7d..2688367b33b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "up" } @@ -136,7 +136,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -159,7 +159,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -182,7 +182,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -205,7 +205,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -228,7 +228,7 @@ "refresh": "onTimeRangeChanged", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -251,7 +251,7 @@ "refresh": "onTimeRangeChanged", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -274,7 +274,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -297,7 +297,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -320,7 +320,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -343,7 +343,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json index 98d2361f0ae..8e57483ceb4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v29.query_variables_refresh_and_options.v42.v2beta1.json @@ -140,7 +140,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -165,7 +165,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -190,7 +190,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -215,7 +215,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -240,7 +240,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -265,7 +265,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -290,7 +290,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -315,7 +315,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -340,7 +340,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, @@ -365,7 +365,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2alpha1.json index 4916dcaa820..db33eea5445 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v3.no-op.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -178,7 +178,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2alpha1.json index 5d81e9bab64..60334f9ffb1 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v30.value_mappings_and_tooltip_options.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -162,7 +162,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -210,7 +210,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -259,7 +259,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -307,7 +307,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -428,7 +428,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -479,7 +479,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2alpha1.json index 1ad2c6f1b29..ac34174bfcb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v31.labels_to_fields_merge.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -105,7 +105,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -194,7 +194,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -238,7 +238,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -376,7 +376,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -449,7 +449,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2alpha1.json index 1d68e967a49..3c58166f6c4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -31,7 +31,7 @@ "kind": "AnnotationQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": {} }, "enable": true, @@ -59,7 +59,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -124,7 +124,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -297,7 +297,7 @@ "refresh": "never", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json index b80ef22e794..1611cea2464 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v32.no_op_migration.v42.v2beta1.json @@ -306,7 +306,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": {} }, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2alpha1.json index c38887d458c..d41d2d7bda3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v33.panel_ds_name_to_ref.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "description": "Target with UID reference should migrate to full object" } @@ -92,7 +92,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "description": "Nested target should also migrate from string to object" } @@ -138,7 +138,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": { "description": "Target with existing object should remain unchanged" } @@ -184,7 +184,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": {} }, "datasource": { @@ -228,7 +228,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -272,7 +272,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "description": "Null target datasource should remain null" } @@ -289,7 +289,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "description": "Valid string should migrate to object" } @@ -306,7 +306,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "description": "Non-existing datasource should be preserved as-is (migration returns nil)" } @@ -323,7 +323,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "description": "Target without datasource field should remain unchanged" } @@ -369,7 +369,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "description": "Existing object target should remain unchanged" } @@ -386,7 +386,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "description": "String target should migrate to object" } @@ -403,7 +403,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "description": "Default datasource string should migrate to object" } @@ -449,7 +449,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "description": "Empty string target should also migrate to empty object {}" } @@ -491,7 +491,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "description": "Unknown target datasource should remain unchanged (migration returns nil)" } @@ -508,7 +508,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "description": "Empty string target should migrate to {}" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2alpha1.json index 963c1e4b649..ad69702a739 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v34.multiple_stats_cloudwatch.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -35,7 +35,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -61,7 +61,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -87,7 +87,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -112,7 +112,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -138,7 +138,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -163,7 +163,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -189,7 +189,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -206,7 +206,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -232,7 +232,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -258,7 +258,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -284,7 +284,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -309,7 +309,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -335,7 +335,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -361,7 +361,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -387,7 +387,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -413,7 +413,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -450,7 +450,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-123456" @@ -476,7 +476,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-123456" @@ -502,7 +502,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-123456" @@ -557,7 +557,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-missing-fields" @@ -611,7 +611,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-with-expression" @@ -637,7 +637,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-with-expression" @@ -692,7 +692,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-insights" @@ -746,7 +746,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-null-stats" @@ -770,7 +770,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-null-stats" @@ -795,7 +795,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-null-stats" @@ -820,7 +820,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-null-stats" @@ -874,7 +874,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-invalid-only" @@ -899,7 +899,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-invalid-only" @@ -924,7 +924,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-invalid-only" @@ -949,7 +949,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "InstanceId": "i-invalid-only" @@ -1003,7 +1003,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "cpu_usage" } @@ -1049,7 +1049,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "LoadBalancer": "my-load-balancer" @@ -1103,7 +1103,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "DBInstanceIdentifier": "my-db" @@ -1157,7 +1157,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "QueueName": "my-queue" @@ -1182,7 +1182,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "up" } @@ -1199,7 +1199,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "TopicName": "my-topic" @@ -1224,7 +1224,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "QueueName": "my-queue" @@ -1278,7 +1278,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "BucketName": "my-bucket" @@ -1331,7 +1331,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "FunctionName": "my-function" @@ -1356,7 +1356,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "FunctionName": "my-function" @@ -1381,7 +1381,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "FunctionName": "my-function" @@ -1405,7 +1405,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "FunctionName": "my-function" @@ -1430,7 +1430,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "FunctionName": "my-function" @@ -1484,7 +1484,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "StreamName": "my-stream" @@ -1509,7 +1509,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "StreamName": "my-stream" @@ -1534,7 +1534,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "StreamName": "my-stream" @@ -1588,7 +1588,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "ClusterName": "my-cluster" @@ -1614,7 +1614,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "dimensions": { "ClusterName": "my-cluster" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2alpha1.json index 936aeb8c30f..c06dd85bb25 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v35.ensure_x_axis_visibility.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -136,7 +136,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -197,7 +197,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -241,7 +241,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -302,7 +302,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -375,7 +375,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -423,7 +423,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2alpha1.json index 17a351eaa46..474b22d4ffb 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v36.ds_name_to_ref.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -35,7 +35,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": false, @@ -52,7 +52,7 @@ "uid": "existing-target-uid" }, "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "enable": false, @@ -69,7 +69,7 @@ "uid": "existing-target-uid" }, "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "enable": false, @@ -86,7 +86,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": false, @@ -103,7 +103,7 @@ "uid": "unknown-datasource-name" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": false, @@ -131,7 +131,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -175,7 +175,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -219,7 +219,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -263,7 +263,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -307,7 +307,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -351,7 +351,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -395,7 +395,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -410,7 +410,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -454,7 +454,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -498,7 +498,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -542,7 +542,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -557,7 +557,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "__expr__", "spec": {} }, "datasource": { @@ -601,7 +601,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -645,7 +645,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "datasource": { @@ -922,7 +922,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", @@ -949,7 +949,7 @@ "uid": "existing-target-uid" }, "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "regex": "", @@ -976,7 +976,7 @@ "uid": "existing-target-uid" }, "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": {} }, "regex": "", @@ -1003,7 +1003,7 @@ "uid": "unknown-datasource" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "regex": "", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2alpha1.json index 55f6c000481..eb254828d3e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v37.legend_normalization.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2alpha1.json index 66bb53a1033..92f6ec2ef8e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.table_displaymode_comprehensive.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2alpha1.json index 52b313e6b4b..a1fb5ca81af 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v38.timeseries_table_display_mode.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2alpha1.json index df196e14beb..37eb51fbdff 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v39.transform_timeseries_table.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2alpha1.json index 1a3bd5c0e72..e6701e601c7 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v4.no-op.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2alpha1.json index db8e3a3542a..10f6bbf20f4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_empty_string.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2alpha1.json index bd834847e28..13a901e94af 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_false.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2alpha1.json index 8279dfd765b..04b94fc3919 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_not_set.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2alpha1.json index 2cc922fc5f5..103fe15fdf3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_numeric.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2alpha1.json index d9347950655..9fe42b778dd 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_string.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2alpha1.json index 9601af1797a..3524796f0aa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v40.refresh_true.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2alpha1.json index 6f34667e603..9036e54f22d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.no_time_picker.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2alpha1.json index 46a07c16b0e..78e27ac4411 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_no_time_options.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2alpha1.json index 15368de002c..8115f687203 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v41.time_picker_time_options.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2alpha1.json index d2e8c73301e..ee97abbd7e2 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.harky_must.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2alpha1.json index 9ef09aa0a9b..d231c6561ad 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v42.hidefrom_tooltip.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2alpha1.json index 5855379dc98..3c5f977cf19 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v5.no-op.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2alpha1.json index de1bac8f2f5..5c98e01c0f0 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -35,7 +35,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "enable": true, @@ -55,7 +55,7 @@ "uid": "loki-uid" }, "query": { - "kind": "DataQuery", + "kind": "loki", "spec": {} }, "enable": false, @@ -86,7 +86,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "cpu_usage{environment=\"$environment\", service=\"$service\"}" } @@ -132,7 +132,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "memory_usage{region=\"$region\"}" } @@ -239,7 +239,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up, instance)" } @@ -264,7 +264,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up, instance)" } @@ -317,7 +317,7 @@ "refresh": "onDashboardLoad", "skipUrlSync": false, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up, instance)" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json index 3562d0154e1..19fbf9be88f 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v6.pulldowns_and_templating.v42.v2beta1.json @@ -273,7 +273,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(up, instance)" @@ -328,7 +328,7 @@ "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "__legacyStringValue": "label_values(up, instance)" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2alpha1.json index 48cdb34c46b..425516baea9 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v7.timepicker.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "up" } @@ -63,7 +63,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "cpu_usage" } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2alpha1.json index 75a2970f8b6..0bf433b534a 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v8.influxdb_query.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "groupBy": [ { @@ -143,7 +143,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "groupBy": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2alpha1.json index 472d28ffa99..0e4ea43800d 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/migrated_dashboards_output/v1beta1-mig-v9.no-op.v42.v2alpha1.json @@ -14,7 +14,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": {} }, "enable": true, @@ -46,7 +46,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -90,7 +90,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { @@ -134,7 +134,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": {} }, "datasource": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json index 9324355a72f..2430b59063b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.annotation-conversions.v2alpha1.json @@ -17,7 +17,7 @@ "uid": "-- Grafana --" }, "query": { - "kind": "DataQuery", + "kind": "grafana", "spec": { "limit": 100, "matchAny": false, @@ -43,7 +43,7 @@ "uid": "existing-ref-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "ALERTS{alertstate=\"firing\"}", "interval": "1m", @@ -69,7 +69,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "lines": 10, "refId": "Anno", @@ -94,7 +94,7 @@ "uid": "existing-target-uid" }, "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": { "query": "tags:deployment", "refId": "Anno", @@ -123,7 +123,7 @@ "uid": "non-default-test-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "format": "table", "rawSql": "SELECT time, title, text, tags FROM annotations WHERE $__timeFilter(time)", @@ -148,7 +148,7 @@ "kind": "AnnotationQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "expr": "static_annotation_query", "refId": "Anno" @@ -172,7 +172,7 @@ "uid": "existing-ref-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "disabled_query", "refId": "Anno" @@ -196,7 +196,7 @@ "uid": "influx-uid" }, "query": { - "kind": "DataQuery", + "kind": "influxdb", "spec": { "query": "from(bucket: \"events\") |\u003e range(start: $__timeFrom, stop: $__timeTo) |\u003e filter(fn: (r) =\u003e r._measurement == \"deployments\")", "refId": "Anno" @@ -234,7 +234,7 @@ "uid": "cloudwatch-uid" }, "query": { - "kind": "DataQuery", + "kind": "cloudwatch", "spec": { "dimensions": { "InstanceId": "i-1234567890abcdef" @@ -264,7 +264,7 @@ "uid": "default-ds-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "ALERTS{alertstate=\"firing\"}", "interval": "1m", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2alpha1.json index 40810adb95b..156969fa5d3 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.dashboard-properties.v2alpha1.json @@ -13,7 +13,7 @@ "kind": "AnnotationQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": { "refId": "Anno" } @@ -45,7 +45,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "", "spec": {} }, "refId": "A", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2alpha1.json index a291ccbc6b2..9e0d5378f9b 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-conversions.v2alpha1.json @@ -34,7 +34,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "up", "interval": "30s" @@ -52,7 +52,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "expr": "cpu_usage", "interval": "1m" @@ -159,7 +159,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "scenarioId": "random_walk" } @@ -205,7 +205,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "loki", "spec": { "format": "table", "rawSql": "SELECT * FROM metrics" @@ -259,7 +259,7 @@ "kind": "PanelQuery", "spec": { "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": { "bucketAggs": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json index 556c3774d34..a4740089365 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json @@ -104,7 +104,7 @@ "uid": "existing-ref-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "label_values(up, instance)" } @@ -335,7 +335,7 @@ "uid": "existing-ref-uid" }, "query": { - "kind": "DataQuery", + "kind": "prometheus", "spec": { "__legacyStringValue": "up" } @@ -374,7 +374,7 @@ "uid": "existing-target-uid" }, "query": { - "kind": "DataQuery", + "kind": "elasticsearch", "spec": { "bucketAggs": [ { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json index 3306c334c5e..18d0371f1c4 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v0alpha1.json @@ -85,7 +85,7 @@ "id": 1, "title": "Panel with Conditional Rendering", "description": "This panel demonstrates conditional rendering features", - "links": null, + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -100,6 +100,7 @@ } }, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "refId": "A", @@ -241,7 +242,7 @@ ] } }, - "links": null, + "links": [], "liveNow": true, "preload": true, "tags": [ @@ -512,7 +513,12 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json index c55f325789d..abbcc39b4a6 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v1beta1.json @@ -85,7 +85,7 @@ "id": 1, "title": "Panel with Conditional Rendering", "description": "This panel demonstrates conditional rendering features", - "links": null, + "links": [], "data": { "kind": "QueryGroup", "spec": { @@ -100,6 +100,7 @@ } }, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "refId": "A", @@ -241,7 +242,7 @@ ] } }, - "links": null, + "links": [], "liveNow": true, "preload": true, "tags": [ @@ -512,7 +513,12 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json index 6d044300534..fda30922928 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.complete.v2beta1.json @@ -79,7 +79,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "datasource": { "name": "gdev-prometheus" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json index 4497423cc53..50b2b4e9038 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v0alpha1.json @@ -222,6 +222,7 @@ } }, "datasource": { + "type": "grafana-testdata-datasource", "uid": "gdev-testdata" }, "refId": "A", @@ -525,6 +526,7 @@ } }, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "refId": "A", @@ -639,6 +641,7 @@ } }, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "refId": "A", @@ -653,6 +656,7 @@ "spec": {} }, "datasource": { + "type": "grafana-testdata-datasource", "uid": "gdev-testdata" }, "refId": "B", @@ -957,13 +961,14 @@ "spec": { "name": "variable-ds-prometheus", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", "skipUrlSync": false, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "query": { @@ -985,8 +990,8 @@ "spec": { "name": "variable-no-ds", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", @@ -1011,8 +1016,8 @@ "spec": { "name": "variable-no-ds-empty-query", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", @@ -1034,8 +1039,8 @@ "spec": { "name": "variable-no-default-ds", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", @@ -1056,7 +1061,12 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json index 11151fc1345..7c01f35fc6c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v1beta1.json @@ -222,6 +222,7 @@ } }, "datasource": { + "type": "grafana-testdata-datasource", "uid": "gdev-testdata" }, "refId": "A", @@ -525,6 +526,7 @@ } }, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "refId": "A", @@ -639,6 +641,7 @@ } }, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "refId": "A", @@ -653,6 +656,7 @@ "spec": {} }, "datasource": { + "type": "grafana-testdata-datasource", "uid": "gdev-testdata" }, "refId": "B", @@ -957,13 +961,14 @@ "spec": { "name": "variable-ds-prometheus", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", "skipUrlSync": false, "datasource": { + "type": "prometheus", "uid": "gdev-prometheus" }, "query": { @@ -985,8 +990,8 @@ "spec": { "name": "variable-no-ds", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", @@ -1011,8 +1016,8 @@ "spec": { "name": "variable-no-ds-empty-query", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", @@ -1034,8 +1039,8 @@ "spec": { "name": "variable-no-default-ds", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", @@ -1056,7 +1061,12 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json index 7e8f0151ebb..09e35c64258 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.ds-data-query.v2beta1.json @@ -58,7 +58,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "lines": 10, @@ -103,7 +103,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "expr": "{action=\"add_client\"}", @@ -213,7 +213,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "grafana-testdata-datasource", "version": "v0", "datasource": { "name": "gdev-testdata" @@ -324,7 +324,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "grafana-testdata-datasource", "version": "v0", "spec": { "scenarioId": "random_walk", @@ -403,7 +403,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "disableTextWrap": false, @@ -518,7 +518,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "datasource": { "name": "gdev-prometheus" @@ -636,7 +636,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "datasource": { "name": "gdev-prometheus" @@ -661,7 +661,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "grafana-testdata-datasource", "version": "v0", "datasource": { "name": "gdev-testdata" @@ -769,7 +769,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "disableTextWrap": false, @@ -791,7 +791,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "grafana-testdata-datasource", "version": "v0", "spec": {} }, @@ -976,15 +976,15 @@ "spec": { "name": "variable-ds-prometheus", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "datasource": { "name": "gdev-prometheus" @@ -1006,15 +1006,15 @@ "spec": { "name": "variable-no-ds", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "grafana-testdata-datasource", "version": "v0", "spec": { "csv": "1,2,3,4", @@ -1034,15 +1034,15 @@ "spec": { "name": "variable-no-ds-empty-query", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "grafana-testdata-datasource", "version": "v0", "spec": {} }, @@ -1059,15 +1059,15 @@ "spec": { "name": "variable-no-default-ds", "current": { - "text": null, - "value": null + "text": "", + "value": "" }, "hide": "dontHide", "refresh": "", "skipUrlSync": false, "query": { "kind": "DataQuery", - "group": "", + "group": "prometheus", "version": "v0", "spec": { "expr": "up" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json index ae50ed46c5d..d03d476b881 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v0alpha1.json @@ -17,11 +17,11 @@ "name": "test-v2alpha1-groupby-adhoc-vars" }, "spec": { - "annotations": null, + "annotations": [], "cursorSync": "", - "elements": null, + "elements": {}, "layout": null, - "links": null, + "links": [], "preload": false, "tags": null, "timeSettings": { @@ -101,7 +101,12 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json index 4f7ce909299..3913d2e898e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.groupby-adhoc-vars.v1beta1.json @@ -17,11 +17,11 @@ "name": "test-v2alpha1-groupby-adhoc-vars" }, "spec": { - "annotations": null, + "annotations": [], "cursorSync": "", - "elements": null, + "elements": {}, "layout": null, - "links": null, + "links": [], "preload": false, "tags": null, "timeSettings": { @@ -101,7 +101,12 @@ } ] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json index acde79a0bcc..a79eaa1528c 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v0alpha1.json @@ -17,7 +17,7 @@ "name": "test-v2alpha1-viz-config" }, "spec": { - "annotations": null, + "annotations": [], "cursorSync": "", "elements": { "panel-1": { @@ -42,6 +42,7 @@ } }, "datasource": { + "type": "grafana-testdata-datasource", "uid": "gdev-testdata" }, "refId": "A", @@ -218,9 +219,14 @@ "fiscalYearStartMonth": 0 }, "title": "Test: V2alpha1 dashboard with viz config", - "variables": null + "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json index e0812fa1a2b..3dbb07b2c65 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v1beta1.json @@ -17,7 +17,7 @@ "name": "test-v2alpha1-viz-config" }, "spec": { - "annotations": null, + "annotations": [], "cursorSync": "", "elements": { "panel-1": { @@ -42,6 +42,7 @@ } }, "datasource": { + "type": "grafana-testdata-datasource", "uid": "gdev-testdata" }, "refId": "A", @@ -218,9 +219,14 @@ "fiscalYearStartMonth": 0 }, "title": "Test: V2alpha1 dashboard with viz config", - "variables": null + "variables": [] }, - "status": {} + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } } } } diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json index ae0dc06ab00..a7f48d67563 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2alpha1.viz-config.v2beta1.json @@ -24,7 +24,7 @@ "spec": { "query": { "kind": "DataQuery", - "group": "", + "group": "grafana-testdata-datasource", "version": "v0", "datasource": { "name": "gdev-testdata" diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v0alpha1.json new file mode 100644 index 00000000000..0ca24342ca3 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v0alpha1.json @@ -0,0 +1,530 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up{job=\"grafana\"}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "onTimeRangeChanged", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "group_by", + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "filters", + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v1beta1.json new file mode 100644 index 00000000000..1bed1975d2a --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v1beta1.json @@ -0,0 +1,530 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up{job=\"grafana\"}" + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "onTimeRangeChanged", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "group_by", + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "filters", + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v2alpha1.json new file mode 100644 index 00000000000..5091df59fcf --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.complete.v2alpha1.json @@ -0,0 +1,504 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-complete", + "labels": { + "category": "test" + }, + "annotations": { + "description": "Complete example of v2alpha1 dashboard features" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "changes(process_start_time_seconds[1m])", + "refId": "Anno" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "Prometheus Annotations", + "builtIn": false + } + } + ], + "cursorSync": "Tooltip", + "description": "This dashboard demonstrates all features that need to be converted from v2alpha1 to v2beta1", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Panel with Conditional Rendering", + "description": "This panel demonstrates conditional rendering features", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "up{job=\"grafana\"}" + } + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [ + { + "kind": "reduce", + "spec": { + "id": "reduce", + "options": { + "includeTimeField": false, + "mode": "reduceFields", + "reducers": [ + "mean" + ] + } + } + } + ], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "type": "value", + "options": { + "0": { + "text": "Down", + "color": "red" + }, + "1": { + "text": "Up", + "color": "green" + } + } + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "red" + }, + { + "value": 1, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "Row", + "spec": { + "title": "Conditional Row", + "collapse": false, + "hideHeader": false, + "fillScreen": false, + "conditionalRendering": { + "kind": "ConditionalRenderingGroup", + "spec": { + "visibility": "show", + "condition": "and", + "items": [ + { + "kind": "ConditionalRenderingVariable", + "spec": { + "variable": "group_by", + "operator": "includes", + "value": "instance" + } + }, + { + "kind": "ConditionalRenderingData", + "spec": { + "value": true + } + }, + { + "kind": "ConditionalRenderingTimeRangeSize", + "spec": { + "value": "1h" + } + } + ] + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 8, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + } + } + } + ] + } + }, + "links": [], + "liveNow": true, + "preload": true, + "tags": [ + "test", + "example", + "migration" + ], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "10s", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "weekStart": "monday", + "fiscalYearStartMonth": 0 + }, + "title": "Test: Complete V2alpha1 Dashboard Example", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "prometheus_query", + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "label": "Prometheus Query", + "hide": "dontHide", + "refresh": "time", + "skipUrlSync": false, + "description": "Shows all up metrics", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "alphabetical", + "definition": "up", + "options": null, + "multi": true, + "includeAll": true, + "allowCustomValue": false + } + }, + { + "kind": "TextVariable", + "spec": { + "name": "text_var", + "current": { + "selected": true, + "text": "server1", + "value": "server1" + }, + "query": "server1,server2,server3", + "label": "Text Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A simple text variable" + } + }, + { + "kind": "ConstantVariable", + "spec": { + "name": "constant_var", + "query": "production", + "current": { + "selected": true, + "text": "production", + "value": "production" + }, + "label": "Constant", + "hide": "dontHide", + "skipUrlSync": true, + "description": "A constant value" + } + }, + { + "kind": "DatasourceVariable", + "spec": { + "name": "ds_var", + "pluginId": "prometheus", + "refresh": "load", + "regex": "/^gdev-/", + "current": { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + }, + "options": [ + { + "text": "gdev-prometheus", + "value": "gdev-prometheus" + } + ], + "multi": false, + "includeAll": false, + "label": "Datasource", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Select a datasource", + "allowCustomValue": false + } + }, + { + "kind": "IntervalVariable", + "spec": { + "name": "interval", + "query": "1m,5m,10m,30m,1h,6h,12h,1d", + "current": { + "selected": true, + "text": "5m", + "value": "5m" + }, + "options": [ + { + "text": "1m", + "value": "1m" + }, + { + "text": "5m", + "value": "5m" + }, + { + "text": "10m", + "value": "10m" + }, + { + "text": "30m", + "value": "30m" + }, + { + "text": "1h", + "value": "1h" + }, + { + "text": "6h", + "value": "6h" + }, + { + "text": "12h", + "value": "12h" + }, + { + "text": "1d", + "value": "1d" + } + ], + "auto": true, + "auto_min": "10s", + "auto_count": 30, + "refresh": "load", + "label": "Interval", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Time interval selection" + } + }, + { + "kind": "CustomVariable", + "spec": { + "name": "custom_var", + "query": "prod : Production, staging : Staging, dev : Development", + "current": { + "text": [ + "Production" + ], + "value": [ + "prod" + ] + }, + "options": [ + { + "text": "Production", + "value": "prod" + }, + { + "text": "Staging", + "value": "staging" + }, + { + "text": "Development", + "value": "dev" + } + ], + "multi": true, + "includeAll": true, + "allValue": "*", + "label": "Custom Options", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Custom multi-value variable", + "allowCustomValue": true + } + }, + { + "kind": "GroupByVariable", + "spec": { + "name": "group_by", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "instance", + "value": "instance" + }, + "options": null, + "multi": false, + "label": "Group By", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Group metrics by label" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "filters", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "baseFilters": [ + { + "key": "job", + "operator": "=", + "value": "grafana", + "condition": "AND" + } + ], + "filters": [], + "defaultKeys": [ + { + "text": "job", + "value": "job", + "expandable": true + }, + { + "text": "instance", + "value": "instance", + "expandable": true + } + ], + "label": "Filters", + "hide": "dontHide", + "skipUrlSync": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v0alpha1.json new file mode 100644 index 00000000000..843139d96cd --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v0alpha1.json @@ -0,0 +1,1107 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-postgresql-datasource", + "version": "v0", + "datasource": { + "name": "PBBCEC2D313BC06C3" + }, + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "gdev-elasticsearch" + }, + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds-empty-query", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-default-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v1beta1.json new file mode 100644 index 00000000000..7e08757d7a4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v1beta1.json @@ -0,0 +1,1107 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "PD8C576611E62080A" + }, + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-postgresql-datasource", + "version": "v0", + "datasource": { + "name": "PBBCEC2D313BC06C3" + }, + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "elasticsearch", + "version": "v0", + "datasource": { + "name": "gdev-elasticsearch" + }, + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "stat", + "version": "12.1.0-pre", + "spec": { + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds-empty-query", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "spec": {} + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-default-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "DataQuery", + "group": "prometheus", + "version": "v0", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v2alpha1.json new file mode 100644 index 00000000000..e4496d650f4 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.ds-data-query.v2alpha1.json @@ -0,0 +1,1058 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-annotations" + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "query": { + "kind": "grafana", + "spec": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + } + }, + "enable": true, + "hide": false, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations \u0026 Alerts", + "builtIn": true + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "blue", + "name": "no-ds-testdata-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "expr": "{action=\"add_client\"}", + "interval": "", + "lines": 10, + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "yellow", + "name": "no-ds-prom-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PBBCEC2D313BC06C3" + }, + "query": { + "kind": "grafana-postgresql-datasource", + "spec": { + "editorMode": "builder", + "format": "table", + "lines": 10, + "rawSql": "", + "refId": "Anno", + "scenarioId": "annotations", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "postgress-annos", + "builtIn": false + } + }, + { + "kind": "AnnotationQuery", + "spec": { + "datasource": { + "type": "elasticsearch", + "uid": "gdev-elasticsearch" + }, + "query": { + "kind": "elasticsearch", + "spec": { + "lines": 10, + "query": "test query", + "refId": "Anno", + "scenarioId": "annotations" + } + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "elastic - annos", + "builtIn": false, + "legacyOptions": { + "tagsField": "asd", + "textField": "asd", + "timeEndField": "asdas", + "timeField": "asd" + } + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "id": 2, + "title": "Simple stat (NO DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 4 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "stat", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + } + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "id": 3, + "title": "Panel with NO REF to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "id": 4, + "title": "Panel with ref to gdev-prometheus", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "id": 5, + "title": "Mixed DS WITH REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + }, + "panel-6": { + "kind": "Panel", + "spec": { + "id": 6, + "title": "Mixed DS WITHOUT REFS", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "prometheus", + "spec": { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(counters_requests{server=\"backend-01\"}[$__rate_interval])", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "useBackend": false + } + }, + "refId": "A", + "hidden": false + } + }, + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "B", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with annotations", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "variable-ds-prometheus", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "csv": "1,2,3,4", + "scenarioId": "csv_metric_values" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-ds-empty-query", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + }, + { + "kind": "QueryVariable", + "spec": { + "name": "variable-no-default-ds", + "current": { + "text": "", + "value": "" + }, + "hide": "dontHide", + "refresh": "", + "skipUrlSync": false, + "query": { + "kind": "prometheus", + "spec": { + "expr": "up" + } + }, + "regex": "", + "sort": "", + "options": null, + "multi": false, + "includeAll": false, + "allowCustomValue": false + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v0alpha1.json new file mode 100644 index 00000000000..fc314270471 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v0alpha1.json @@ -0,0 +1,113 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": {}, + "layout": null, + "links": [], + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "", + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A group by variable" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "datasource-3" + }, + "spec": { + "name": "adhocVar", + "baseFilters": [ + { + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" + } + ], + "filters": [ + { + "key": "key3", + "operator": "=", + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true + } + ], + "label": "Adhoc Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "An adhoc variable", + "allowCustomValue": true + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v1beta1.json new file mode 100644 index 00000000000..7925848b4da --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v1beta1.json @@ -0,0 +1,113 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": {}, + "layout": null, + "links": [], + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "group": "prometheus", + "datasource": { + "name": "gdev-prometheus" + }, + "spec": { + "name": "", + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A group by variable" + } + }, + { + "kind": "AdhocVariable", + "group": "prometheus", + "datasource": { + "name": "datasource-3" + }, + "spec": { + "name": "adhocVar", + "baseFilters": [ + { + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" + } + ], + "filters": [ + { + "key": "key3", + "operator": "=", + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true + } + ], + "label": "Adhoc Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "An adhoc variable", + "allowCustomValue": true + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v2alpha1.json new file mode 100644 index 00000000000..5cb430e9655 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.groupby-adhoc-vars.v2alpha1.json @@ -0,0 +1,98 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-groupby-adhoc-vars" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": {}, + "layout": null, + "links": [], + "preload": false, + "tags": null, + "timeSettings": { + "from": "", + "to": "", + "autoRefresh": "", + "autoRefreshIntervals": null, + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with group by and adhoc variables", + "variables": [ + { + "kind": "GroupByVariable", + "spec": { + "name": "", + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "current": { + "text": "text7", + "value": "value7" + }, + "options": null, + "multi": false, + "label": "Group By Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "A group by variable" + } + }, + { + "kind": "AdhocVariable", + "spec": { + "name": "adhocVar", + "datasource": { + "type": "prometheus", + "uid": "datasource-3" + }, + "baseFilters": [ + { + "key": "key1", + "operator": "=", + "value": "value1", + "condition": "AND" + }, + { + "key": "key2", + "operator": "=", + "value": "value2", + "condition": "OR" + } + ], + "filters": [ + { + "key": "key3", + "operator": "=", + "value": "value3", + "condition": "AND" + } + ], + "defaultKeys": [ + { + "text": "defaultKey1", + "value": "defaultKey1", + "group": "defaultGroup1", + "expandable": true + } + ], + "label": "Adhoc Variable", + "hide": "dontHide", + "skipUrlSync": false, + "description": "An adhoc variable", + "allowCustomValue": true + } + } + ] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v0alpha1.json new file mode 100644 index 00000000000..c792c0537aa --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v0alpha1.json @@ -0,0 +1,235 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v0alpha1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with viz config", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v1beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v1beta1.json new file mode 100644 index 00000000000..94df0c2e468 --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v1beta1.json @@ -0,0 +1,235 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v1beta1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": null, + "status": { + "conversion": { + "failed": true, + "error": "backend conversion not yet implemented", + "storedVersion": "v2beta1", + "source": { + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2beta1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana-testdata-datasource", + "version": "v0", + "datasource": { + "name": "gdev-testdata" + }, + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "timeseries", + "version": "12.1.0-pre", + "spec": { + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with viz config", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2alpha1" + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v2alpha1.json new file mode 100644 index 00000000000..48c2aab46af --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v2beta1.viz-config.v2alpha1.json @@ -0,0 +1,218 @@ +{ + "kind": "Dashboard", + "apiVersion": "dashboard.grafana.app/v2alpha1", + "metadata": { + "name": "test-v2alpha1-viz-config" + }, + "spec": { + "annotations": [], + "cursorSync": "", + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Simple timeseries (WITH DS REF)", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "grafana-testdata-datasource", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 3 + } + }, + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "gdev-testdata" + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "timeseries", + "spec": { + "pluginVersion": "12.1.0-pre", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + }, + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-6" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-6h", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Test: V2alpha1 dashboard with viz config", + "variables": [] + }, + "status": { + "conversion": { + "failed": false, + "storedVersion": "v2beta1" + } + } +} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 6b8448867f1..21877cb6361 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -1058,8 +1058,11 @@ func buildDataQueryKind(query interface{}, datasourceType string) dashv2alpha1.D querySpec = make(map[string]interface{}) } + // Use datasourceType as the kind (datasource type, e.g., "prometheus", "elasticsearch") + kind := datasourceType + return dashv2alpha1.DashboardDataQueryKind{ - Kind: "DataQuery", + Kind: kind, Spec: querySpec, } } @@ -1082,8 +1085,11 @@ func buildDataQueryKindForVariable(query interface{}, datasourceType string) das querySpec = make(map[string]interface{}) } + // Use datasourceType as the kind (datasource type, e.g., "prometheus", "elasticsearch") + kind := datasourceType + return dashv2alpha1.DashboardDataQueryKind{ - Kind: "DataQuery", + Kind: kind, Spec: querySpec, } } @@ -1629,9 +1635,11 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da // Build the query from target var query *dashv2alpha1.DashboardDataQueryKind + // Use datasourceType as the kind (datasource type, e.g., "prometheus", "grafana") + kind := datasourceType if target, ok := annotationMap["target"].(map[string]interface{}); ok && target != nil { queryKind := dashv2alpha1.DashboardDataQueryKind{ - Kind: "DataQuery", + Kind: kind, Spec: target, } // Group field is not available in v2alpha1 DashboardDataQueryKind @@ -1639,7 +1647,7 @@ func buildAnnotationQuery(annotationMap map[string]interface{}) (dashv2alpha1.Da } else { // Always provide a query to match frontend behavior queryKind := dashv2alpha1.DashboardDataQueryKind{ - Kind: "DataQuery", + Kind: kind, Spec: map[string]interface{}{}, } // Group field is not available in v2alpha1 DashboardDataQueryKind diff --git a/apps/dashboard/pkg/migration/conversion/v2.go b/apps/dashboard/pkg/migration/conversion/v2.go index db1824e725c..3b27c130d50 100644 --- a/apps/dashboard/pkg/migration/conversion/v2.go +++ b/apps/dashboard/pkg/migration/conversion/v2.go @@ -109,14 +109,23 @@ func Convert_V2beta1_to_V1beta1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard func Convert_V2beta1_to_V2alpha1(in *dashv2beta1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta - // TODO: implement v2beta1 to V2alpha1 conversion + if err := ConvertDashboard_V2beta1_to_V2alpha1(in, out, scope); err != nil { + out.Status = dashv2alpha1.DashboardStatus{ + Conversion: &dashv2alpha1.DashboardConversionStatus{ + StoredVersion: ptr.To(dashv2beta1.VERSION), + Failed: true, + Error: ptr.To(err.Error()), + Source: in, + }, + } + + return NewConversionError(err.Error(), "v2beta1", "v2alpha1", "ConvertDashboard_V2beta1_to_V2alpha1") + } out.Status = dashv2alpha1.DashboardStatus{ Conversion: &dashv2alpha1.DashboardConversionStatus{ StoredVersion: ptr.To(dashv2beta1.VERSION), - Failed: true, - Error: ptr.To("backend conversion not yet implemented"), - Source: in, + Failed: false, }, } diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index 9243d246cf0..e5bd1f834d3 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -132,22 +132,28 @@ func convertDataQuery_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardDataQueryKin out.Kind = "DataQuery" out.Version = "v0" out.Spec = in.Spec - // Don't set group field by default - only set if datasource is present } - // Convert datasource reference and set group from datasource type + // Convert datasource reference and set group + // Priority: 1) datasource.Type, 2) in.Kind (if no datasource or datasource.Type is empty) if datasource != nil { out.Datasource = &dashv2beta1.DashboardV2beta1DataQueryKindDatasource{} if datasource.Uid != nil { out.Datasource.Name = datasource.Uid } - // Set group to datasource type to match frontend behavior - // Only set group if datasource type is not empty (matches frontend logic) + // Set group to datasource type if available if datasource.Type != nil && *datasource.Type != "" { out.Group = *datasource.Type } } + // Fallback: if group is still empty and we have a query with a kind (datasource type), use the kind as group + // This preserves the datasource type information when datasource reference is missing + // Note: in.Kind is the datasource type (e.g., "prometheus", "elasticsearch"), not "DataQuery" + if out.Group == "" && in != nil && in.Kind != "" { + out.Group = in.Kind + } + return nil } @@ -761,11 +767,20 @@ func convertQueryVariableSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQuer func convertVariableOption_V2alpha1_to_V2beta1(in dashv2alpha1.DashboardVariableOption) dashv2beta1.DashboardVariableOption { return dashv2beta1.DashboardVariableOption{ Selected: in.Selected, - Text: dashv2beta1.DashboardStringOrArrayOfString(in.Text), - Value: dashv2beta1.DashboardStringOrArrayOfString(in.Value), + Text: convertStringOrArrayOfString_V2alpha1_to_V2beta1(in.Text), + Value: convertStringOrArrayOfString_V2alpha1_to_V2beta1(in.Value), } } +// convertStringOrArrayOfString_V2alpha1_to_V2beta1 preserves empty strings to match unmarshaler behavior +func convertStringOrArrayOfString_V2alpha1_to_V2beta1(in dashv2alpha1.DashboardStringOrArrayOfString) dashv2beta1.DashboardStringOrArrayOfString { + out := dashv2beta1.DashboardStringOrArrayOfString{ + ArrayOfString: in.ArrayOfString, + } + out.String = in.String + return out +} + func convertVariableOptionPtr_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableOption) *dashv2beta1.DashboardVariableOption { if in == nil { return nil @@ -916,83 +931,37 @@ func convertConditionalRenderingGroupKind_V2alpha1_to_V2beta1(in *dashv2alpha1.D } func convertRowLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, scope conversion.Scope) error { - // Handle the different union type orderings by converting through the main layout function - // Create a temporary variable with the correct type ordering - var tempIn dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind - var tempOut dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind - - // Copy the layout data with the correct ordering if in.GridLayoutKind != nil { - tempIn.GridLayoutKind = in.GridLayoutKind - } - if in.RowsLayoutKind != nil { - tempIn.RowsLayoutKind = in.RowsLayoutKind + out.GridLayoutKind = &dashv2beta1.DashboardGridLayoutKind{ + Kind: in.GridLayoutKind.Kind, + } + return convertGridLayoutSpec_V2alpha1_to_V2beta1(&in.GridLayoutKind.Spec, &out.GridLayoutKind.Spec, scope) } + if in.AutoGridLayoutKind != nil { - tempIn.AutoGridLayoutKind = in.AutoGridLayoutKind + out.AutoGridLayoutKind = &dashv2beta1.DashboardAutoGridLayoutKind{ + Kind: in.AutoGridLayoutKind.Kind, + } + return convertAutoGridLayoutSpec_V2alpha1_to_V2beta1(&in.AutoGridLayoutKind.Spec, &out.AutoGridLayoutKind.Spec, scope) } + if in.TabsLayoutKind != nil { - tempIn.TabsLayoutKind = in.TabsLayoutKind + out.TabsLayoutKind = &dashv2beta1.DashboardTabsLayoutKind{ + Kind: in.TabsLayoutKind.Kind, + } + return convertTabsLayoutSpec_V2alpha1_to_V2beta1(&in.TabsLayoutKind.Spec, &out.TabsLayoutKind.Spec, scope) } - if err := convertLayout_V2alpha1_to_V2beta1(&tempIn, &tempOut, scope); err != nil { - return err - } - - // Copy back to the output with the correct ordering - if tempOut.GridLayoutKind != nil { - out.GridLayoutKind = tempOut.GridLayoutKind - } - if tempOut.RowsLayoutKind != nil { - out.RowsLayoutKind = tempOut.RowsLayoutKind - } - if tempOut.AutoGridLayoutKind != nil { - out.AutoGridLayoutKind = tempOut.AutoGridLayoutKind - } - if tempOut.TabsLayoutKind != nil { - out.TabsLayoutKind = tempOut.TabsLayoutKind + if in.RowsLayoutKind != nil { + out.RowsLayoutKind = &dashv2beta1.DashboardRowsLayoutKind{ + Kind: in.RowsLayoutKind.Kind, + } + return convertRowsLayoutSpec_V2alpha1_to_V2beta1(&in.RowsLayoutKind.Spec, &out.RowsLayoutKind.Spec, scope) } return nil } func convertTabLayout_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error { - // Handle the different union type orderings by converting through the main layout function - // Create a temporary variable with the correct type ordering - var tempIn dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind - var tempOut dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind - - // Copy the layout data with the correct ordering - if in.GridLayoutKind != nil { - tempIn.GridLayoutKind = in.GridLayoutKind - } - if in.RowsLayoutKind != nil { - tempIn.RowsLayoutKind = in.RowsLayoutKind - } - if in.AutoGridLayoutKind != nil { - tempIn.AutoGridLayoutKind = in.AutoGridLayoutKind - } - if in.TabsLayoutKind != nil { - tempIn.TabsLayoutKind = in.TabsLayoutKind - } - - if err := convertLayout_V2alpha1_to_V2beta1(&tempIn, &tempOut, scope); err != nil { - return err - } - - // Copy back to the output with the correct ordering - if tempOut.GridLayoutKind != nil { - out.GridLayoutKind = tempOut.GridLayoutKind - } - if tempOut.RowsLayoutKind != nil { - out.RowsLayoutKind = tempOut.RowsLayoutKind - } - if tempOut.AutoGridLayoutKind != nil { - out.AutoGridLayoutKind = tempOut.AutoGridLayoutKind - } - if tempOut.TabsLayoutKind != nil { - out.TabsLayoutKind = tempOut.TabsLayoutKind - } - - return nil + return convertLayout_V2alpha1_to_V2beta1(in, out, scope) } diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go new file mode 100644 index 00000000000..580fef1b3ea --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -0,0 +1,1007 @@ +package conversion + +import ( + "k8s.io/apimachinery/pkg/conversion" + + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" +) + +// Schema Migration: v2beta1 → v2alpha1 +// +// This file handles the conversion from Dashboard v2beta1 to v2alpha1 schema. +// This is the reverse of v2alpha1_to_v2beta1.go conversion. +// The main changes to reverse are: +// +// 1. DataQueryKind Structure Changes: +// - v2beta1: kind = "DataQuery" (hardcoded), group = datasource type, version = "v0" +// - v2alpha1: kind = datasource type (from group), datasource at spec level +// +// 2. Datasource Reference Migration: +// - v2beta1: Datasource references inside DataQueryKind.datasource +// - v2alpha1: Datasource references at spec level (PanelQuerySpec.datasource, AnnotationQuerySpec.datasource, etc.) +// - v2beta1: {name?: string} → v2alpha1: {type?: string, uid?: string} +// +// 3. Query Requirements: +// - v2beta1: AnnotationQuerySpec.query is required +// - v2alpha1: AnnotationQuerySpec.query? is optional (can be nil) +// - If query is empty/default, set to nil in v2alpha1 +// +// 4. VizConfig Changes: +// - v2beta1: kind = "VizConfig", group = panel type, version = plugin version +// - v2alpha1: kind = panel type (from group), spec.pluginVersion (from version) +// +// 5. DataSourceRef Usage: +// - v2beta1: Kept only for backward compatibility in GroupBy and Adhoc variables +// - v2alpha1: Used widely across different specs +// +// The conversion preserves all dashboard functionality while restructuring +// the data model to move datasource references from DataQueryKind to spec level. + +func ConvertDashboard_V2beta1_to_V2alpha1(in *dashv2beta1.Dashboard, out *dashv2alpha1.Dashboard, scope conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.APIVersion = dashv2alpha1.APIVERSION + out.Kind = in.Kind + + return convertDashboardSpec_V2beta1_to_V2alpha1(&in.Spec, &out.Spec, scope) +} + +func convertDashboardSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardSpec, out *dashv2alpha1.DashboardSpec, scope conversion.Scope) error { + // Convert annotations + out.Annotations = make([]dashv2alpha1.DashboardAnnotationQueryKind, len(in.Annotations)) + for i := range in.Annotations { + if err := convertAnnotationQuery_V2beta1_to_V2alpha1(&in.Annotations[i], &out.Annotations[i], scope); err != nil { + return err + } + } + + // Copy simple fields + out.CursorSync = dashv2alpha1.DashboardDashboardCursorSync(in.CursorSync) + out.Description = in.Description + out.Editable = in.Editable + out.LiveNow = in.LiveNow + out.Preload = in.Preload + out.Revision = in.Revision + out.Tags = in.Tags + out.Title = in.Title + + // Convert elements + out.Elements = make(map[string]dashv2alpha1.DashboardElement, len(in.Elements)) + for key, element := range in.Elements { + var convertedElement dashv2alpha1.DashboardElement + if err := convertElement_V2beta1_to_V2alpha1(&element, &convertedElement, scope); err != nil { + return err + } + out.Elements[key] = convertedElement + } + + // Convert layout + if err := convertLayout_V2beta1_to_V2alpha1(&in.Layout, &out.Layout, scope); err != nil { + return err + } + + // Convert links + out.Links = make([]dashv2alpha1.DashboardDashboardLink, len(in.Links)) + for i, link := range in.Links { + convertDashboardLink_V2beta1_to_V2alpha1(&link, &out.Links[i]) + } + + // Convert time settings + if err := convertTimeSettings_V2beta1_to_V2alpha1(&in.TimeSettings, &out.TimeSettings, scope); err != nil { + return err + } + + // Convert variables + out.Variables = make([]dashv2alpha1.DashboardVariableKind, len(in.Variables)) + for i, variable := range in.Variables { + if err := convertVariable_V2beta1_to_V2alpha1(&variable, &out.Variables[i], scope); err != nil { + return err + } + } + + return nil +} + +func convertAnnotationQuery_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardAnnotationQueryKind, out *dashv2alpha1.DashboardAnnotationQueryKind, scope conversion.Scope) error { + out.Kind = in.Kind + + // Convert spec + out.Spec.Enable = in.Spec.Enable + out.Spec.Hide = in.Spec.Hide + out.Spec.IconColor = in.Spec.IconColor + out.Spec.Name = in.Spec.Name + out.Spec.BuiltIn = in.Spec.BuiltIn + out.Spec.Filter = (*dashv2alpha1.DashboardAnnotationPanelFilter)(in.Spec.Filter) + out.Spec.LegacyOptions = in.Spec.LegacyOptions + + // Convert query - move datasource from query back to annotation spec + query, datasource, err := convertDataQuery_V2beta1_to_V2alpha1(&in.Spec.Query, scope) + if err != nil { + return err + } + out.Spec.Query = query + out.Spec.Datasource = datasource + + return nil +} + +func convertDataQuery_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardDataQueryKind, scope conversion.Scope) (*dashv2alpha1.DashboardDataQueryKind, *dashv2alpha1.DashboardDataSourceRef, error) { + if in == nil { + return nil, nil, nil + } + + // Extract datasource from query + var datasource *dashv2alpha1.DashboardDataSourceRef + + // Use Group as queryKind - this is the primary source of truth for the query kind in v2beta1 + // Group field contains the datasource type in v2beta1 + queryKind := in.Group + + if in.Datasource != nil && in.Datasource.Name != nil { + uid := *in.Datasource.Name + + datasource = &dashv2alpha1.DashboardDataSourceRef{ + Uid: &uid, + Type: &in.Group, + } + } + + // Convert query kind: v2beta1 has kind="DataQuery", v2alpha1 uses group as kind + // If queryKind is still empty, it means we couldn't determine it + + // Check if query spec is empty (default query created for annotations) + // If it's empty and there's no datasource, return nil for query (v2alpha1 allows nil for annotations) + if len(in.Spec) == 0 && datasource == nil { + return nil, nil, nil + } + + query := &dashv2alpha1.DashboardDataQueryKind{ + Kind: queryKind, + Spec: in.Spec, + } + + return query, datasource, nil +} + +func convertElement_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardElement, out *dashv2alpha1.DashboardElement, scope conversion.Scope) error { + if in.PanelKind != nil { + out.PanelKind = &dashv2alpha1.DashboardPanelKind{} + return convertPanelKind_V2beta1_to_V2alpha1(in.PanelKind, out.PanelKind, scope) + } + + if in.LibraryPanelKind != nil { + out.LibraryPanelKind = &dashv2alpha1.DashboardLibraryPanelKind{} + return convertLibraryPanelKind_V2beta1_to_V2alpha1(in.LibraryPanelKind, out.LibraryPanelKind, scope) + } + + return nil +} + +func convertPanelKind_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardPanelKind, out *dashv2alpha1.DashboardPanelKind, scope conversion.Scope) error { + out.Kind = in.Kind + + // Convert spec + out.Spec.Id = in.Spec.Id + out.Spec.Title = in.Spec.Title + out.Spec.Description = in.Spec.Description + out.Spec.Transparent = in.Spec.Transparent + + // Convert links + out.Spec.Links = make([]dashv2alpha1.DashboardDataLink, len(in.Spec.Links)) + for i, link := range in.Spec.Links { + convertDataLink_V2beta1_to_V2alpha1(&link, &out.Spec.Links[i]) + } + + // Convert data (QueryGroup) + if err := convertQueryGroup_V2beta1_to_V2alpha1(&in.Spec.Data, &out.Spec.Data, scope); err != nil { + return err + } + + // Convert vizConfig + if err := convertVizConfig_V2beta1_to_V2alpha1(&in.Spec.VizConfig, &out.Spec.VizConfig); err != nil { + return err + } + + return nil +} + +func convertLibraryPanelKind_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardLibraryPanelKind, out *dashv2alpha1.DashboardLibraryPanelKind, scope conversion.Scope) error { + out.Kind = in.Kind + out.Spec.Id = in.Spec.Id + out.Spec.Title = in.Spec.Title + out.Spec.LibraryPanel = dashv2alpha1.DashboardLibraryPanelRef{ + Name: in.Spec.LibraryPanel.Name, + Uid: in.Spec.LibraryPanel.Uid, + } + return nil +} + +func convertQueryGroup_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardQueryGroupKind, out *dashv2alpha1.DashboardQueryGroupKind, scope conversion.Scope) error { + out.Kind = in.Kind + + // Convert queries + out.Spec.Queries = make([]dashv2alpha1.DashboardPanelQueryKind, len(in.Spec.Queries)) + for i, query := range in.Spec.Queries { + if err := convertPanelQuery_V2beta1_to_V2alpha1(&query, &out.Spec.Queries[i], scope); err != nil { + return err + } + } + + // Convert transformations + out.Spec.Transformations = make([]dashv2alpha1.DashboardTransformationKind, len(in.Spec.Transformations)) + for i, transformation := range in.Spec.Transformations { + convertTransformation_V2beta1_to_V2alpha1(&transformation, &out.Spec.Transformations[i]) + } + + // Convert query options + convertQueryOptions_V2beta1_to_V2alpha1(&in.Spec.QueryOptions, &out.Spec.QueryOptions) + + return nil +} + +func convertPanelQuery_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardPanelQueryKind, out *dashv2alpha1.DashboardPanelQueryKind, scope conversion.Scope) error { + out.Kind = in.Kind // PanelQueryKind keeps "PanelQuery" as its kind + out.Spec.RefId = in.Spec.RefId + out.Spec.Hidden = in.Spec.Hidden + + // Convert query - move datasource from query back to panel query spec + // Panel queries always have a query (unlike annotations which can be nil) + query, datasource, err := convertDataQuery_V2beta1_to_V2alpha1(&in.Spec.Query, scope) + if err != nil { + return err + } + // For panel queries, always create a query (even if empty) + if query == nil { + // Create default query with empty spec + query = &dashv2alpha1.DashboardDataQueryKind{ + Kind: in.Spec.Query.Group, + Spec: make(map[string]interface{}), + } + } + out.Spec.Query = *query + out.Spec.Datasource = datasource + + return nil +} + +func convertTransformation_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardTransformationKind, out *dashv2alpha1.DashboardTransformationKind) { + out.Kind = in.Kind + out.Spec.Id = in.Spec.Id + out.Spec.Disabled = in.Spec.Disabled + out.Spec.Filter = (*dashv2alpha1.DashboardMatcherConfig)(in.Spec.Filter) + out.Spec.Topic = (*dashv2alpha1.DashboardDataTopic)(in.Spec.Topic) + out.Spec.Options = in.Spec.Options +} + +func convertQueryOptions_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardQueryOptionsSpec, out *dashv2alpha1.DashboardQueryOptionsSpec) { + out.TimeFrom = in.TimeFrom + out.MaxDataPoints = in.MaxDataPoints + out.TimeShift = in.TimeShift + out.QueryCachingTTL = in.QueryCachingTTL + out.Interval = in.Interval + out.CacheTimeout = in.CacheTimeout + out.HideTimeOverride = in.HideTimeOverride +} + +func convertVizConfig_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardVizConfigKind, out *dashv2alpha1.DashboardVizConfigKind) error { + // v2beta1: kind="VizConfig", group=panel type, version=plugin version + // v2alpha1: kind=panel type, spec.pluginVersion + out.Kind = in.Group // panel type from group + out.Spec.PluginVersion = in.Version + out.Spec.Options = in.Spec.Options + + // Convert field config + convertFieldConfigSource_V2beta1_to_V2alpha1(&in.Spec.FieldConfig, &out.Spec.FieldConfig) + + return nil +} + +func convertFieldConfigSource_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardFieldConfigSource, out *dashv2alpha1.DashboardFieldConfigSource) { + // Convert defaults + convertFieldConfig_V2beta1_to_V2alpha1(&in.Defaults, &out.Defaults) + + // Convert overrides + out.Overrides = make([]dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides, len(in.Overrides)) + for i, override := range in.Overrides { + convertFieldConfigOverride_V2beta1_to_V2alpha1(&override, &out.Overrides[i]) + } +} + +func convertFieldConfig_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardFieldConfig, out *dashv2alpha1.DashboardFieldConfig) { + *out = dashv2alpha1.DashboardFieldConfig{ + DisplayName: in.DisplayName, + DisplayNameFromDS: in.DisplayNameFromDS, + Description: in.Description, + Path: in.Path, + Writeable: in.Writeable, + Filterable: in.Filterable, + Unit: in.Unit, + Decimals: in.Decimals, + Min: in.Min, + Max: in.Max, + Links: in.Links, + NoValue: in.NoValue, + Custom: in.Custom, + } + + // Convert thresholds + if in.Thresholds != nil { + out.Thresholds = &dashv2alpha1.DashboardThresholdsConfig{ + Mode: dashv2alpha1.DashboardThresholdsMode(in.Thresholds.Mode), + Steps: make([]dashv2alpha1.DashboardThreshold, len(in.Thresholds.Steps)), + } + for i, step := range in.Thresholds.Steps { + // Preserve null values from v2beta1 + out.Thresholds.Steps[i] = dashv2alpha1.DashboardThreshold{ + Value: step.Value, + Color: step.Color, + } + } + } + + // Convert color + if in.Color != nil { + out.Color = &dashv2alpha1.DashboardFieldColor{ + Mode: dashv2alpha1.DashboardFieldColorModeId(in.Color.Mode), + FixedColor: in.Color.FixedColor, + SeriesBy: (*dashv2alpha1.DashboardFieldColorSeriesByMode)(in.Color.SeriesBy), + } + } + + // Convert mappings + if in.Mappings != nil { + out.Mappings = make([]dashv2alpha1.DashboardValueMapping, len(in.Mappings)) + for i, mapping := range in.Mappings { + convertValueMapping_V2beta1_to_V2alpha1(&mapping, &out.Mappings[i]) + } + } +} + +func convertFieldConfigOverride_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardV2beta1FieldConfigSourceOverrides, out *dashv2alpha1.DashboardV2alpha1FieldConfigSourceOverrides) { + out.SystemRef = in.SystemRef + + out.Matcher = dashv2alpha1.DashboardMatcherConfig{ + Id: in.Matcher.Id, + Options: in.Matcher.Options, + } + + out.Properties = make([]dashv2alpha1.DashboardDynamicConfigValue, len(in.Properties)) + for i, prop := range in.Properties { + out.Properties[i] = dashv2alpha1.DashboardDynamicConfigValue{ + Id: prop.Id, + Value: prop.Value, + } + } +} + +func convertValueMapping_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardValueMapping, out *dashv2alpha1.DashboardValueMapping) { + if in.ValueMap != nil { + out.ValueMap = &dashv2alpha1.DashboardValueMap{ + Type: dashv2alpha1.DashboardMappingType(in.ValueMap.Type), + Options: make(map[string]dashv2alpha1.DashboardValueMappingResult, len(in.ValueMap.Options)), + } + for k, v := range in.ValueMap.Options { + out.ValueMap.Options[k] = dashv2alpha1.DashboardValueMappingResult{ + Text: v.Text, + Color: v.Color, + Icon: v.Icon, + Index: v.Index, + } + } + } + + if in.RangeMap != nil { + out.RangeMap = &dashv2alpha1.DashboardRangeMap{ + Type: dashv2alpha1.DashboardMappingType(in.RangeMap.Type), + Options: dashv2alpha1.DashboardV2alpha1RangeMapOptions{ + From: in.RangeMap.Options.From, + To: in.RangeMap.Options.To, + Result: dashv2alpha1.DashboardValueMappingResult{ + Text: in.RangeMap.Options.Result.Text, + Color: in.RangeMap.Options.Result.Color, + Icon: in.RangeMap.Options.Result.Icon, + Index: in.RangeMap.Options.Result.Index, + }, + }, + } + } + + if in.RegexMap != nil { + out.RegexMap = &dashv2alpha1.DashboardRegexMap{ + Type: dashv2alpha1.DashboardMappingType(in.RegexMap.Type), + Options: dashv2alpha1.DashboardV2alpha1RegexMapOptions{ + Pattern: in.RegexMap.Options.Pattern, + Result: dashv2alpha1.DashboardValueMappingResult{ + Text: in.RegexMap.Options.Result.Text, + Color: in.RegexMap.Options.Result.Color, + Icon: in.RegexMap.Options.Result.Icon, + Index: in.RegexMap.Options.Result.Index, + }, + }, + } + } + + if in.SpecialValueMap != nil { + out.SpecialValueMap = &dashv2alpha1.DashboardSpecialValueMap{ + Type: dashv2alpha1.DashboardMappingType(in.SpecialValueMap.Type), + Options: dashv2alpha1.DashboardV2alpha1SpecialValueMapOptions{ + Match: dashv2alpha1.DashboardSpecialValueMatch(in.SpecialValueMap.Options.Match), + Result: dashv2alpha1.DashboardValueMappingResult{ + Text: in.SpecialValueMap.Options.Result.Text, + Color: in.SpecialValueMap.Options.Result.Color, + Icon: in.SpecialValueMap.Options.Result.Icon, + Index: in.SpecialValueMap.Options.Result.Index, + }, + }, + } + } +} + +func convertLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error { + if in.GridLayoutKind != nil { + out.GridLayoutKind = &dashv2alpha1.DashboardGridLayoutKind{ + Kind: in.GridLayoutKind.Kind, + } + return convertGridLayoutSpec_V2beta1_to_V2alpha1(&in.GridLayoutKind.Spec, &out.GridLayoutKind.Spec, scope) + } + + if in.RowsLayoutKind != nil { + out.RowsLayoutKind = &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: in.RowsLayoutKind.Kind, + } + return convertRowsLayoutSpec_V2beta1_to_V2alpha1(&in.RowsLayoutKind.Spec, &out.RowsLayoutKind.Spec, scope) + } + + if in.AutoGridLayoutKind != nil { + out.AutoGridLayoutKind = &dashv2alpha1.DashboardAutoGridLayoutKind{ + Kind: in.AutoGridLayoutKind.Kind, + } + return convertAutoGridLayoutSpec_V2beta1_to_V2alpha1(&in.AutoGridLayoutKind.Spec, &out.AutoGridLayoutKind.Spec, scope) + } + + if in.TabsLayoutKind != nil { + out.TabsLayoutKind = &dashv2alpha1.DashboardTabsLayoutKind{ + Kind: in.TabsLayoutKind.Kind, + } + return convertTabsLayoutSpec_V2beta1_to_V2alpha1(&in.TabsLayoutKind.Spec, &out.TabsLayoutKind.Spec, scope) + } + + return nil +} + +func convertGridLayoutSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutSpec, out *dashv2alpha1.DashboardGridLayoutSpec, scope conversion.Scope) error { + out.Items = make([]dashv2alpha1.DashboardGridLayoutItemKind, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = dashv2alpha1.DashboardGridLayoutItemKind{ + Kind: item.Kind, + Spec: dashv2alpha1.DashboardGridLayoutItemSpec{ + X: item.Spec.X, + Y: item.Spec.Y, + Width: item.Spec.Width, + Height: item.Spec.Height, + Element: dashv2alpha1.DashboardElementReference{ + Kind: item.Spec.Element.Kind, + Name: item.Spec.Element.Name, + }, + Repeat: convertRepeatOptions_V2beta1_to_V2alpha1(item.Spec.Repeat), + }, + } + } + return nil +} + +func convertRowsLayoutSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardRowsLayoutSpec, out *dashv2alpha1.DashboardRowsLayoutSpec, scope conversion.Scope) error { + out.Rows = make([]dashv2alpha1.DashboardRowsLayoutRowKind, len(in.Rows)) + for i, row := range in.Rows { + out.Rows[i] = dashv2alpha1.DashboardRowsLayoutRowKind{ + Kind: row.Kind, + Spec: dashv2alpha1.DashboardRowsLayoutRowSpec{ + Title: row.Spec.Title, + Collapse: row.Spec.Collapse, + HideHeader: row.Spec.HideHeader, + FillScreen: row.Spec.FillScreen, + ConditionalRendering: convertConditionalRenderingGroupKind_V2beta1_to_V2alpha1(row.Spec.ConditionalRendering), + Repeat: convertRowRepeatOptions_V2beta1_to_V2alpha1(row.Spec.Repeat), + }, + } + if err := convertRowLayout_V2beta1_to_V2alpha1(&row.Spec.Layout, &out.Rows[i].Spec.Layout, scope); err != nil { + return err + } + } + return nil +} + +func convertAutoGridLayoutSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardAutoGridLayoutSpec, out *dashv2alpha1.DashboardAutoGridLayoutSpec, scope conversion.Scope) error { + out.MaxColumnCount = in.MaxColumnCount + out.ColumnWidthMode = dashv2alpha1.DashboardAutoGridLayoutSpecColumnWidthMode(in.ColumnWidthMode) + out.ColumnWidth = in.ColumnWidth + out.RowHeightMode = dashv2alpha1.DashboardAutoGridLayoutSpecRowHeightMode(in.RowHeightMode) + out.RowHeight = in.RowHeight + out.FillScreen = in.FillScreen + + out.Items = make([]dashv2alpha1.DashboardAutoGridLayoutItemKind, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = dashv2alpha1.DashboardAutoGridLayoutItemKind{ + Kind: item.Kind, + Spec: dashv2alpha1.DashboardAutoGridLayoutItemSpec{ + Element: dashv2alpha1.DashboardElementReference{ + Kind: item.Spec.Element.Kind, + Name: item.Spec.Element.Name, + }, + Repeat: convertAutoGridRepeatOptions_V2beta1_to_V2alpha1(item.Spec.Repeat), + ConditionalRendering: convertConditionalRenderingGroupKind_V2beta1_to_V2alpha1(item.Spec.ConditionalRendering), + }, + } + } + return nil +} + +func convertTabsLayoutSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardTabsLayoutSpec, out *dashv2alpha1.DashboardTabsLayoutSpec, scope conversion.Scope) error { + out.Tabs = make([]dashv2alpha1.DashboardTabsLayoutTabKind, len(in.Tabs)) + for i, tab := range in.Tabs { + out.Tabs[i] = dashv2alpha1.DashboardTabsLayoutTabKind{ + Kind: tab.Kind, + Spec: dashv2alpha1.DashboardTabsLayoutTabSpec{ + Title: tab.Spec.Title, + ConditionalRendering: convertConditionalRenderingGroupKind_V2beta1_to_V2alpha1(tab.Spec.ConditionalRendering), + Repeat: convertTabRepeatOptions_V2beta1_to_V2alpha1(tab.Spec.Repeat), + }, + } + if err := convertTabLayout_V2beta1_to_V2alpha1(&tab.Spec.Layout, &out.Tabs[i].Spec.Layout, scope); err != nil { + return err + } + } + return nil +} + +func convertDashboardLink_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardDashboardLink, out *dashv2alpha1.DashboardDashboardLink) { + out.Title = in.Title + out.Type = dashv2alpha1.DashboardDashboardLinkType(in.Type) + out.Icon = in.Icon + out.Tooltip = in.Tooltip + out.Url = in.Url + out.Tags = in.Tags + out.AsDropdown = in.AsDropdown + out.TargetBlank = in.TargetBlank + out.IncludeVars = in.IncludeVars + out.KeepTime = in.KeepTime +} + +func convertDataLink_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardDataLink, out *dashv2alpha1.DashboardDataLink) { + out.Title = in.Title + out.Url = in.Url + out.TargetBlank = in.TargetBlank +} + +func convertTimeSettings_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardTimeSettingsSpec, out *dashv2alpha1.DashboardTimeSettingsSpec, scope conversion.Scope) error { + out.Timezone = in.Timezone + out.From = in.From + out.To = in.To + out.AutoRefresh = in.AutoRefresh + out.AutoRefreshIntervals = in.AutoRefreshIntervals + out.HideTimepicker = in.HideTimepicker + out.WeekStart = (*dashv2alpha1.DashboardTimeSettingsSpecWeekStart)(in.WeekStart) + out.FiscalYearStartMonth = in.FiscalYearStartMonth + out.NowDelay = in.NowDelay + + // Convert quick ranges + if in.QuickRanges != nil { + out.QuickRanges = make([]dashv2alpha1.DashboardTimeRangeOption, len(in.QuickRanges)) + for i, qr := range in.QuickRanges { + out.QuickRanges[i] = dashv2alpha1.DashboardTimeRangeOption{ + Display: qr.Display, + From: qr.From, + To: qr.To, + } + } + } + + return nil +} + +func convertVariable_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardVariableKind, out *dashv2alpha1.DashboardVariableKind, scope conversion.Scope) error { + if in.QueryVariableKind != nil { + out.QueryVariableKind = &dashv2alpha1.DashboardQueryVariableKind{ + Kind: in.QueryVariableKind.Kind, + } + return convertQueryVariableSpec_V2beta1_to_V2alpha1(&in.QueryVariableKind.Spec, &out.QueryVariableKind.Spec, scope) + } + + if in.TextVariableKind != nil { + out.TextVariableKind = &dashv2alpha1.DashboardTextVariableKind{ + Kind: in.TextVariableKind.Kind, + Spec: dashv2alpha1.DashboardTextVariableSpec{ + Name: in.TextVariableKind.Spec.Name, + Current: convertVariableOption_V2beta1_to_V2alpha1(in.TextVariableKind.Spec.Current), + Query: in.TextVariableKind.Spec.Query, + Label: in.TextVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.TextVariableKind.Spec.Hide), + SkipUrlSync: in.TextVariableKind.Spec.SkipUrlSync, + Description: in.TextVariableKind.Spec.Description, + }, + } + } + + if in.ConstantVariableKind != nil { + out.ConstantVariableKind = &dashv2alpha1.DashboardConstantVariableKind{ + Kind: in.ConstantVariableKind.Kind, + Spec: dashv2alpha1.DashboardConstantVariableSpec{ + Name: in.ConstantVariableKind.Spec.Name, + Query: in.ConstantVariableKind.Spec.Query, + Current: convertVariableOption_V2beta1_to_V2alpha1(in.ConstantVariableKind.Spec.Current), + Label: in.ConstantVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.ConstantVariableKind.Spec.Hide), + SkipUrlSync: in.ConstantVariableKind.Spec.SkipUrlSync, + Description: in.ConstantVariableKind.Spec.Description, + }, + } + } + + if in.DatasourceVariableKind != nil { + out.DatasourceVariableKind = &dashv2alpha1.DashboardDatasourceVariableKind{ + Kind: in.DatasourceVariableKind.Kind, + Spec: dashv2alpha1.DashboardDatasourceVariableSpec{ + Name: in.DatasourceVariableKind.Spec.Name, + PluginId: in.DatasourceVariableKind.Spec.PluginId, + Refresh: dashv2alpha1.DashboardVariableRefresh(in.DatasourceVariableKind.Spec.Refresh), + Regex: in.DatasourceVariableKind.Spec.Regex, + Current: convertVariableOption_V2beta1_to_V2alpha1(in.DatasourceVariableKind.Spec.Current), + Options: convertVariableOptions_V2beta1_to_V2alpha1(in.DatasourceVariableKind.Spec.Options), + Multi: in.DatasourceVariableKind.Spec.Multi, + IncludeAll: in.DatasourceVariableKind.Spec.IncludeAll, + AllValue: in.DatasourceVariableKind.Spec.AllValue, + Label: in.DatasourceVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.DatasourceVariableKind.Spec.Hide), + SkipUrlSync: in.DatasourceVariableKind.Spec.SkipUrlSync, + Description: in.DatasourceVariableKind.Spec.Description, + AllowCustomValue: in.DatasourceVariableKind.Spec.AllowCustomValue, + }, + } + } + + if in.IntervalVariableKind != nil { + out.IntervalVariableKind = &dashv2alpha1.DashboardIntervalVariableKind{ + Kind: in.IntervalVariableKind.Kind, + Spec: dashv2alpha1.DashboardIntervalVariableSpec{ + Name: in.IntervalVariableKind.Spec.Name, + Query: in.IntervalVariableKind.Spec.Query, + Current: convertVariableOption_V2beta1_to_V2alpha1(in.IntervalVariableKind.Spec.Current), + Options: convertVariableOptions_V2beta1_to_V2alpha1(in.IntervalVariableKind.Spec.Options), + Auto: in.IntervalVariableKind.Spec.Auto, + AutoMin: in.IntervalVariableKind.Spec.AutoMin, + AutoCount: in.IntervalVariableKind.Spec.AutoCount, + Refresh: dashv2alpha1.DashboardVariableRefresh("load"), // Interval variables always use "load" in v2alpha1 + Label: in.IntervalVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.IntervalVariableKind.Spec.Hide), + SkipUrlSync: in.IntervalVariableKind.Spec.SkipUrlSync, + Description: in.IntervalVariableKind.Spec.Description, + }, + } + } + + if in.CustomVariableKind != nil { + out.CustomVariableKind = &dashv2alpha1.DashboardCustomVariableKind{ + Kind: in.CustomVariableKind.Kind, + Spec: dashv2alpha1.DashboardCustomVariableSpec{ + Name: in.CustomVariableKind.Spec.Name, + Query: in.CustomVariableKind.Spec.Query, + Current: convertVariableOption_V2beta1_to_V2alpha1(in.CustomVariableKind.Spec.Current), + Options: convertVariableOptions_V2beta1_to_V2alpha1(in.CustomVariableKind.Spec.Options), + Multi: in.CustomVariableKind.Spec.Multi, + IncludeAll: in.CustomVariableKind.Spec.IncludeAll, + AllValue: in.CustomVariableKind.Spec.AllValue, + Label: in.CustomVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.CustomVariableKind.Spec.Hide), + SkipUrlSync: in.CustomVariableKind.Spec.SkipUrlSync, + Description: in.CustomVariableKind.Spec.Description, + AllowCustomValue: in.CustomVariableKind.Spec.AllowCustomValue, + }, + } + } + + if in.GroupByVariableKind != nil { + // Extract datasource from top-level fields + var dsType *string + var dsUID *string + if in.GroupByVariableKind.Datasource != nil && in.GroupByVariableKind.Datasource.Name != nil { + dsUID = in.GroupByVariableKind.Datasource.Name + } + if in.GroupByVariableKind.Group != "" { + dsType = &in.GroupByVariableKind.Group + } + + var datasource *dashv2alpha1.DashboardDataSourceRef + if dsType != nil || dsUID != nil { + datasource = &dashv2alpha1.DashboardDataSourceRef{ + Type: dsType, + Uid: dsUID, + } + } + + out.GroupByVariableKind = &dashv2alpha1.DashboardGroupByVariableKind{ + Kind: in.GroupByVariableKind.Kind, + Spec: dashv2alpha1.DashboardGroupByVariableSpec{ + Name: in.GroupByVariableKind.Spec.Name, + DefaultValue: convertVariableOptionPtr_V2beta1_to_V2alpha1(in.GroupByVariableKind.Spec.DefaultValue), + Current: convertVariableOption_V2beta1_to_V2alpha1(in.GroupByVariableKind.Spec.Current), + Options: convertVariableOptions_V2beta1_to_V2alpha1(in.GroupByVariableKind.Spec.Options), + Multi: in.GroupByVariableKind.Spec.Multi, + Label: in.GroupByVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.GroupByVariableKind.Spec.Hide), + SkipUrlSync: in.GroupByVariableKind.Spec.SkipUrlSync, + Description: in.GroupByVariableKind.Spec.Description, + Datasource: datasource, + }, + } + } + + if in.AdhocVariableKind != nil { + // Extract datasource from top-level fields + var dsType *string + var dsUID *string + if in.AdhocVariableKind.Datasource != nil && in.AdhocVariableKind.Datasource.Name != nil { + dsUID = in.AdhocVariableKind.Datasource.Name + } + if in.AdhocVariableKind.Group != "" { + dsType = &in.AdhocVariableKind.Group + } + + var datasource *dashv2alpha1.DashboardDataSourceRef + if dsType != nil || dsUID != nil { + datasource = &dashv2alpha1.DashboardDataSourceRef{ + Type: dsType, + Uid: dsUID, + } + } + + out.AdhocVariableKind = &dashv2alpha1.DashboardAdhocVariableKind{ + Kind: in.AdhocVariableKind.Kind, + Spec: dashv2alpha1.DashboardAdhocVariableSpec{ + Name: in.AdhocVariableKind.Spec.Name, + BaseFilters: convertAdHocFilters_V2beta1_to_V2alpha1(in.AdhocVariableKind.Spec.BaseFilters), + Filters: convertAdHocFilters_V2beta1_to_V2alpha1(in.AdhocVariableKind.Spec.Filters), + DefaultKeys: convertMetricFindValues_V2beta1_to_V2alpha1(in.AdhocVariableKind.Spec.DefaultKeys), + Label: in.AdhocVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.AdhocVariableKind.Spec.Hide), + SkipUrlSync: in.AdhocVariableKind.Spec.SkipUrlSync, + Description: in.AdhocVariableKind.Spec.Description, + AllowCustomValue: in.AdhocVariableKind.Spec.AllowCustomValue, + Datasource: datasource, + }, + } + } + + return nil +} + +func convertQueryVariableSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardQueryVariableSpec, out *dashv2alpha1.DashboardQueryVariableSpec, scope conversion.Scope) error { + out.Name = in.Name + out.Current = convertVariableOption_V2beta1_to_V2alpha1(in.Current) + out.Label = in.Label + out.Hide = dashv2alpha1.DashboardVariableHide(in.Hide) + out.Refresh = dashv2alpha1.DashboardVariableRefresh(in.Refresh) + out.SkipUrlSync = in.SkipUrlSync + out.Description = in.Description + out.Regex = in.Regex + out.Sort = dashv2alpha1.DashboardVariableSort(in.Sort) + out.Definition = in.Definition + out.Options = convertVariableOptions_V2beta1_to_V2alpha1(in.Options) + out.Multi = in.Multi + out.IncludeAll = in.IncludeAll + out.AllValue = in.AllValue + out.Placeholder = in.Placeholder + out.AllowCustomValue = in.AllowCustomValue + out.StaticOptions = convertVariableOptions_V2beta1_to_V2alpha1(in.StaticOptions) + out.StaticOptionsOrder = (*dashv2alpha1.DashboardQueryVariableSpecStaticOptionsOrder)(in.StaticOptionsOrder) + + // Convert query - move datasource from query back to variable spec + // QueryVariableSpec.Query is required (not a pointer) in v2alpha1, so always create it + query, datasource, err := convertDataQuery_V2beta1_to_V2alpha1(&in.Query, scope) + if err != nil { + return err + } + if query == nil { + // Create default query if it was nil (shouldn't happen for query variables, but handle gracefully) + query = &dashv2alpha1.DashboardDataQueryKind{ + Kind: in.Query.Group, + Spec: in.Query.Spec, + } + } + out.Query = *query + out.Datasource = datasource + + return nil +} + +func convertVariableOption_V2beta1_to_V2alpha1(in dashv2beta1.DashboardVariableOption) dashv2alpha1.DashboardVariableOption { + return dashv2alpha1.DashboardVariableOption{ + Selected: in.Selected, + Text: dashv2alpha1.DashboardStringOrArrayOfString(in.Text), + Value: dashv2alpha1.DashboardStringOrArrayOfString(in.Value), + } +} + +func convertVariableOptionPtr_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardVariableOption) *dashv2alpha1.DashboardVariableOption { + if in == nil { + return nil + } + out := convertVariableOption_V2beta1_to_V2alpha1(*in) + return &out +} + +func convertVariableOptions_V2beta1_to_V2alpha1(in []dashv2beta1.DashboardVariableOption) []dashv2alpha1.DashboardVariableOption { + if in == nil { + return nil + } + out := make([]dashv2alpha1.DashboardVariableOption, len(in)) + for i, option := range in { + out[i] = convertVariableOption_V2beta1_to_V2alpha1(option) + } + return out +} + +func convertAdHocFilters_V2beta1_to_V2alpha1(in []dashv2beta1.DashboardAdHocFilterWithLabels) []dashv2alpha1.DashboardAdHocFilterWithLabels { + if in == nil { + return nil + } + out := make([]dashv2alpha1.DashboardAdHocFilterWithLabels, len(in)) + for i, filter := range in { + out[i] = dashv2alpha1.DashboardAdHocFilterWithLabels{ + Key: filter.Key, + Operator: filter.Operator, + Value: filter.Value, + Values: filter.Values, + KeyLabel: filter.KeyLabel, + ValueLabels: filter.ValueLabels, + ForceEdit: filter.ForceEdit, + Origin: filter.Origin, + Condition: filter.Condition, + } + } + return out +} + +func convertMetricFindValues_V2beta1_to_V2alpha1(in []dashv2beta1.DashboardMetricFindValue) []dashv2alpha1.DashboardMetricFindValue { + if in == nil { + return nil + } + out := make([]dashv2alpha1.DashboardMetricFindValue, len(in)) + for i, value := range in { + out[i] = dashv2alpha1.DashboardMetricFindValue{ + Text: value.Text, + Value: (*dashv2alpha1.DashboardStringOrFloat64)(value.Value), + Group: value.Group, + Expandable: value.Expandable, + } + } + return out +} + +func convertRepeatOptions_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardRepeatOptions) *dashv2alpha1.DashboardRepeatOptions { + if in == nil { + return nil + } + return &dashv2alpha1.DashboardRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + Direction: (*dashv2alpha1.DashboardRepeatOptionsDirection)(in.Direction), + MaxPerRow: in.MaxPerRow, + } +} + +func convertRowRepeatOptions_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardRowRepeatOptions) *dashv2alpha1.DashboardRowRepeatOptions { + if in == nil { + return nil + } + return &dashv2alpha1.DashboardRowRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + } +} + +func convertAutoGridRepeatOptions_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardAutoGridRepeatOptions) *dashv2alpha1.DashboardAutoGridRepeatOptions { + if in == nil { + return nil + } + return &dashv2alpha1.DashboardAutoGridRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + } +} + +func convertTabRepeatOptions_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardTabRepeatOptions) *dashv2alpha1.DashboardTabRepeatOptions { + if in == nil { + return nil + } + return &dashv2alpha1.DashboardTabRepeatOptions{ + Mode: in.Mode, + Value: in.Value, + } +} + +func convertConditionalRenderingGroupKind_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardConditionalRenderingGroupKind) *dashv2alpha1.DashboardConditionalRenderingGroupKind { + if in == nil { + return nil + } + + out := &dashv2alpha1.DashboardConditionalRenderingGroupKind{ + Kind: in.Kind, + Spec: dashv2alpha1.DashboardConditionalRenderingGroupSpec{ + Visibility: dashv2alpha1.DashboardConditionalRenderingGroupSpecVisibility(in.Spec.Visibility), + Condition: dashv2alpha1.DashboardConditionalRenderingGroupSpecCondition(in.Spec.Condition), + Items: make([]dashv2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind, len(in.Spec.Items)), + }, + } + + // Convert each item in the Items slice + for i, item := range in.Spec.Items { + out.Spec.Items[i] = dashv2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind{} + + if item.ConditionalRenderingVariableKind != nil { + out.Spec.Items[i].ConditionalRenderingVariableKind = &dashv2alpha1.DashboardConditionalRenderingVariableKind{ + Kind: item.ConditionalRenderingVariableKind.Kind, + Spec: dashv2alpha1.DashboardConditionalRenderingVariableSpec{ + Variable: item.ConditionalRenderingVariableKind.Spec.Variable, + Operator: dashv2alpha1.DashboardConditionalRenderingVariableSpecOperator(item.ConditionalRenderingVariableKind.Spec.Operator), + Value: item.ConditionalRenderingVariableKind.Spec.Value, + }, + } + } + + if item.ConditionalRenderingDataKind != nil { + out.Spec.Items[i].ConditionalRenderingDataKind = &dashv2alpha1.DashboardConditionalRenderingDataKind{ + Kind: item.ConditionalRenderingDataKind.Kind, + Spec: dashv2alpha1.DashboardConditionalRenderingDataSpec{ + Value: item.ConditionalRenderingDataKind.Spec.Value, + }, + } + } + + if item.ConditionalRenderingTimeRangeSizeKind != nil { + out.Spec.Items[i].ConditionalRenderingTimeRangeSizeKind = &dashv2alpha1.DashboardConditionalRenderingTimeRangeSizeKind{ + Kind: item.ConditionalRenderingTimeRangeSizeKind.Kind, + Spec: dashv2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec{ + Value: item.ConditionalRenderingTimeRangeSizeKind.Spec.Value, + }, + } + } + } + + return out +} + +func convertRowLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, out *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, scope conversion.Scope) error { + if in.GridLayoutKind != nil { + out.GridLayoutKind = &dashv2alpha1.DashboardGridLayoutKind{ + Kind: in.GridLayoutKind.Kind, + } + return convertGridLayoutSpec_V2beta1_to_V2alpha1(&in.GridLayoutKind.Spec, &out.GridLayoutKind.Spec, scope) + } + + if in.AutoGridLayoutKind != nil { + out.AutoGridLayoutKind = &dashv2alpha1.DashboardAutoGridLayoutKind{ + Kind: in.AutoGridLayoutKind.Kind, + } + return convertAutoGridLayoutSpec_V2beta1_to_V2alpha1(&in.AutoGridLayoutKind.Spec, &out.AutoGridLayoutKind.Spec, scope) + } + + if in.TabsLayoutKind != nil { + out.TabsLayoutKind = &dashv2alpha1.DashboardTabsLayoutKind{ + Kind: in.TabsLayoutKind.Kind, + } + return convertTabsLayoutSpec_V2beta1_to_V2alpha1(&in.TabsLayoutKind.Spec, &out.TabsLayoutKind.Spec, scope) + } + + if in.RowsLayoutKind != nil { + out.RowsLayoutKind = &dashv2alpha1.DashboardRowsLayoutKind{ + Kind: in.RowsLayoutKind.Kind, + } + return convertRowsLayoutSpec_V2beta1_to_V2alpha1(&in.RowsLayoutKind.Spec, &out.RowsLayoutKind.Spec, scope) + } + + return nil +} + +func convertTabLayout_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, out *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, scope conversion.Scope) error { + return convertLayout_V2beta1_to_V2alpha1(in, out, scope) +} diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go new file mode 100644 index 00000000000..773961a31fb --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go @@ -0,0 +1,580 @@ +package conversion + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" +) + +// TestV2beta1ToV2alpha1RoundTrip tests round-trip conversion: v2beta1 → v2alpha1 → v2beta1 +// This ensures no data loss during conversion between v2 versions +func TestV2beta1ToV2alpha1RoundTrip(t *testing.T) { + // Initialize the migrator with test providers + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider, leProvider) + require.NoError(t, err) + + // Read all v2beta1 output files (these are converted from v1beta1 or v2alpha1) + outputDir := filepath.Join("testdata", "output") + files, err := os.ReadDir(outputDir) + require.NoError(t, err, "Failed to read output directory") + + for _, file := range files { + if file.IsDir() { + continue + } + + // Only process v2beta1 files + if !strings.Contains(file.Name(), ".v2beta1.json") { + continue + } + + t.Run(fmt.Sprintf("RoundTrip_%s", file.Name()), func(t *testing.T) { + // Read v2beta1 dashboard file + inputFile := filepath.Join(outputDir, file.Name()) + // ignore gosec G304 as this function is only used in the test process + //nolint:gosec + inputData, err := os.ReadFile(inputFile) + require.NoError(t, err, "Failed to read input file") + + // Parse v2beta1 dashboard + var originalV2beta1 dashv2beta1.Dashboard + err = json.Unmarshal(inputData, &originalV2beta1) + require.NoError(t, err, "Failed to unmarshal v2beta1 dashboard") + + // Collect original statistics + originalStats := collectStatsV2beta1(originalV2beta1.Spec) + + // Step 1: Convert v2beta1 → v2alpha1 + var v2alpha1 dashv2alpha1.Dashboard + err = scheme.Convert(&originalV2beta1, &v2alpha1, nil) + require.NoError(t, err, "Failed to convert v2beta1 to v2alpha1") + + // Collect v2alpha1 statistics + v2alpha1Stats := collectStatsV2alpha1(v2alpha1.Spec) + + // Verify no data loss in first conversion + err = detectConversionDataLoss(originalStats, v2alpha1Stats, "V2beta1", "V2alpha1") + assert.NoError(t, err, "Data loss detected in v2beta1 → v2alpha1 conversion") + + // Step 2: Convert v2alpha1 → v2beta1 (back) + var roundTripV2beta1 dashv2beta1.Dashboard + err = scheme.Convert(&v2alpha1, &roundTripV2beta1, nil) + require.NoError(t, err, "Failed to convert v2alpha1 back to v2beta1") + + // Collect round-trip statistics + roundTripStats := collectStatsV2beta1(roundTripV2beta1.Spec) + + // Verify no data loss in round-trip conversion + err = detectConversionDataLoss(originalStats, roundTripStats, "V2beta1", "V2beta1_RoundTrip") + assert.NoError(t, err, "Data loss detected in round-trip conversion") + + // Verify counts match exactly + assert.Equal(t, originalStats.panelCount, roundTripStats.panelCount, "Panel count mismatch after round-trip") + assert.Equal(t, originalStats.queryCount, roundTripStats.queryCount, "Query count mismatch after round-trip") + assert.Equal(t, originalStats.annotationCount, roundTripStats.annotationCount, "Annotation count mismatch after round-trip") + assert.Equal(t, originalStats.linkCount, roundTripStats.linkCount, "Link count mismatch after round-trip") + assert.Equal(t, originalStats.variableCount, roundTripStats.variableCount, "Variable count mismatch after round-trip") + + // Compare the entire v2beta1 spec before and after round-trip - they should be exactly equal + assert.Equal(t, originalV2beta1.Spec, roundTripV2beta1.Spec, "v2beta1 spec should be exactly equal before and after round-trip conversion") + }) + } +} + +// TestV2beta1ToV2alpha1FromOutputFiles tests conversion from v2beta1 output files back to v2alpha1 +// and compares with the original v2alpha1 input files to ensure the conversion is correct. +// This test takes files from output directory that are prefixed with "v2alpha1." and suffixed with ".v2beta1.json", +// converts them from v2beta1 to v2alpha1, and compares the result with the corresponding input file. +func TestV2beta1ToV2alpha1FromOutputFiles(t *testing.T) { + // Initialize the migrator with test providers + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider, leProvider) + require.NoError(t, err) + + // Read all v2beta1 output files that were converted from v2alpha1 + outputDir := filepath.Join("testdata", "output") + files, err := os.ReadDir(outputDir) + require.NoError(t, err, "Failed to read output directory") + + for _, file := range files { + if file.IsDir() { + continue + } + + // Only process files that are prefixed with "v2alpha1." and suffixed with ".v2beta1.json" + // These are v2beta1 files that were converted from v2alpha1 input files + if !strings.HasPrefix(file.Name(), "v2alpha1.") || !strings.HasSuffix(file.Name(), ".v2beta1.json") { + continue + } + + // Extract the base name (e.g., "v2alpha1.complete" from "v2alpha1.complete.v2beta1.json") + baseName := strings.TrimSuffix(file.Name(), ".v2beta1.json") + // The corresponding input file should be "v2alpha1.complete.json" + expectedInputFile := baseName + ".json" + + t.Run(fmt.Sprintf("Convert_%s", file.Name()), func(t *testing.T) { + // Read the v2beta1 output file + v2beta1OutputFile := filepath.Join(outputDir, file.Name()) + // ignore gosec G304 as this function is only used in the test process + //nolint:gosec + v2beta1Data, err := os.ReadFile(v2beta1OutputFile) + require.NoError(t, err, "Failed to read v2beta1 output file") + + // Parse v2beta1 dashboard + var v2beta1 dashv2beta1.Dashboard + err = json.Unmarshal(v2beta1Data, &v2beta1) + require.NoError(t, err, "Failed to unmarshal v2beta1 dashboard") + + // Convert v2beta1 → v2alpha1 + var convertedV2alpha1 dashv2alpha1.Dashboard + err = scheme.Convert(&v2beta1, &convertedV2alpha1, nil) + require.NoError(t, err, "Failed to convert v2beta1 to v2alpha1") + + // Read the original v2alpha1 input file + inputDir := filepath.Join("testdata", "input") + expectedInputPath := filepath.Join(inputDir, expectedInputFile) + // ignore gosec G304 as this function is only used in the test process + //nolint:gosec + expectedInputData, err := os.ReadFile(expectedInputPath) + if err != nil { + t.Skipf("Skipping test: expected input file %s not found", expectedInputFile) + return + } + + // Parse expected v2alpha1 dashboard + var expectedV2alpha1 dashv2alpha1.Dashboard + err = json.Unmarshal(expectedInputData, &expectedV2alpha1) + require.NoError(t, err, "Failed to unmarshal expected v2alpha1 dashboard") + + // Compare statistics to ensure no data loss + expectedStats := collectStatsV2alpha1(expectedV2alpha1.Spec) + convertedStats := collectStatsV2alpha1(convertedV2alpha1.Spec) + + // Verify no data loss using statistics + assert.Equal(t, expectedStats.panelCount, convertedStats.panelCount, "Panel count mismatch") + assert.Equal(t, expectedStats.queryCount, convertedStats.queryCount, "Query count mismatch") + assert.Equal(t, expectedStats.annotationCount, convertedStats.annotationCount, "Annotation count mismatch") + assert.Equal(t, expectedStats.linkCount, convertedStats.linkCount, "Link count mismatch") + assert.Equal(t, expectedStats.variableCount, convertedStats.variableCount, "Variable count mismatch") + + // Compare the entire spec structures - they should be equal + assert.Equal(t, expectedV2alpha1.Spec, convertedV2alpha1.Spec, "Converted v2alpha1 spec should match expected v2alpha1 spec") + }) + } +} + +// TestV2beta1ToV2alpha1 tests the conversion logic for v2beta1 to v2alpha1. +// The conversion uses the Group field directly as the query kind and datasource type. +// No fallback mechanisms (provider lookup or UID inference) are used. +func TestV2beta1ToV2alpha1(t *testing.T) { + // Initialize the migrator with test providers + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider, leProvider) + require.NoError(t, err) + + testCases := []struct { + name string + createV2beta1 func() *dashv2beta1.Dashboard + validateV2alpha1 func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) + }{ + { + name: "dashboard with panels and queries - datasource extraction", + createV2beta1: func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Elements: map[string]dashv2beta1.DashboardElement{ + "panel-1": { + PanelKind: &dashv2beta1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2beta1.DashboardPanelSpec{ + Id: 1, + Title: "Test Panel", + Data: dashv2beta1.DashboardQueryGroupKind{ + Spec: dashv2beta1.DashboardQueryGroupSpec{ + Queries: []dashv2beta1.DashboardPanelQueryKind{ + { + Kind: "PanelQuery", + Spec: dashv2beta1.DashboardPanelQuerySpec{ + RefId: "A", + Hidden: false, + Query: dashv2beta1.DashboardDataQueryKind{ + Kind: "DataQuery", + Version: "v0", + Group: "prometheus", + Datasource: &dashv2beta1.DashboardV2beta1DataQueryKindDatasource{ + Name: stringPtr("prometheus-uid"), + }, + Spec: map[string]interface{}{ + "expr": "up", + }, + }, + }, + }, + }, + }, + }, + VizConfig: dashv2beta1.DashboardVizConfigKind{ + Kind: "VizConfig", + Group: "timeseries", + Version: "1.0.0", + Spec: dashv2beta1.DashboardVizConfigSpec{ + Options: map[string]interface{}{}, + }, + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + assert.Equal(t, "Test Dashboard", v2alpha1.Spec.Title) + assert.NotNil(t, v2alpha1.Spec.Elements["panel-1"]) + panel := v2alpha1.Spec.Elements["panel-1"].PanelKind + require.NotNil(t, panel) + assert.Equal(t, float64(1), panel.Spec.Id) + assert.Equal(t, "Test Panel", panel.Spec.Title) + // Verify datasource was moved from query to panel query spec + require.Len(t, panel.Spec.Data.Spec.Queries, 1) + query := panel.Spec.Data.Spec.Queries[0] + assert.NotNil(t, query.Spec.Datasource) + assert.Equal(t, "prometheus-uid", *query.Spec.Datasource.Uid) + assert.Equal(t, "prometheus", *query.Spec.Datasource.Type) + // Verify query kind was extracted from group + assert.Equal(t, "prometheus", query.Spec.Query.Kind) + // Verify vizConfig was converted + assert.Equal(t, "timeseries", panel.Spec.VizConfig.Kind) + assert.Equal(t, "1.0.0", panel.Spec.VizConfig.Spec.PluginVersion) + }, + }, + { + name: "empty group in panel query - preserved as empty kind", + createV2beta1: func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Elements: map[string]dashv2beta1.DashboardElement{ + "panel-1": { + PanelKind: &dashv2beta1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2beta1.DashboardPanelSpec{ + Id: 1, + Title: "Test Panel", + Data: dashv2beta1.DashboardQueryGroupKind{ + Spec: dashv2beta1.DashboardQueryGroupSpec{ + Queries: []dashv2beta1.DashboardPanelQueryKind{ + { + Kind: "PanelQuery", + Spec: dashv2beta1.DashboardPanelQuerySpec{ + RefId: "A", + Hidden: false, + Query: dashv2beta1.DashboardDataQueryKind{ + Kind: "DataQuery", + Version: "v0", + Group: "", // Empty group + Spec: map[string]interface{}{ + "expr": "up", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + panel := v2alpha1.Spec.Elements["panel-1"].PanelKind + require.NotNil(t, panel) + require.Len(t, panel.Spec.Data.Spec.Queries, 1) + query := panel.Spec.Data.Spec.Queries[0] + assert.Equal(t, "", query.Spec.Query.Kind, "Empty group should result in empty kind") + }, + }, + { + name: "dashboard with annotations - datasource extraction", + createV2beta1: func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Annotations: []dashv2beta1.DashboardAnnotationQueryKind{ + { + Kind: "AnnotationQuery", + Spec: dashv2beta1.DashboardAnnotationQuerySpec{ + Name: "Test Annotation", + Enable: true, + Query: dashv2beta1.DashboardDataQueryKind{ + Kind: "DataQuery", + Version: "v0", + Group: "prometheus", + Datasource: &dashv2beta1.DashboardV2beta1DataQueryKindDatasource{ + Name: stringPtr("prometheus-uid"), + }, + Spec: map[string]interface{}{ + "expr": "up", + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.Len(t, v2alpha1.Spec.Annotations, 1) + annotation := v2alpha1.Spec.Annotations[0] + assert.Equal(t, "Test Annotation", annotation.Spec.Name) + assert.True(t, annotation.Spec.Enable) + // Verify datasource was moved from query to annotation spec + assert.NotNil(t, annotation.Spec.Datasource) + assert.Equal(t, "prometheus-uid", *annotation.Spec.Datasource.Uid) + assert.Equal(t, "prometheus", *annotation.Spec.Datasource.Type) + // Verify query kind was extracted from group + require.NotNil(t, annotation.Spec.Query) + assert.Equal(t, "prometheus", annotation.Spec.Query.Kind) + }, + }, + { + name: "empty group in annotation query - preserved as empty kind", + createV2beta1: func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Annotations: []dashv2beta1.DashboardAnnotationQueryKind{ + { + Kind: "AnnotationQuery", + Spec: dashv2beta1.DashboardAnnotationQuerySpec{ + Name: "Test Annotation", + Enable: true, + Query: dashv2beta1.DashboardDataQueryKind{ + Kind: "DataQuery", + Version: "v0", + Group: "", // Empty group + Spec: map[string]interface{}{ + "expr": "up", + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.Len(t, v2alpha1.Spec.Annotations, 1) + annotation := v2alpha1.Spec.Annotations[0] + require.NotNil(t, annotation.Spec.Query) + assert.Equal(t, "", annotation.Spec.Query.Kind, "Empty group should result in empty kind") + }, + }, + { + name: "dashboard with query variable - datasource extraction", + createV2beta1: func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2beta1.DashboardVariableKind{ + { + QueryVariableKind: &dashv2beta1.DashboardQueryVariableKind{ + Kind: "QueryVariable", + Spec: dashv2beta1.DashboardQueryVariableSpec{ + Name: "testVar", + Query: dashv2beta1.DashboardDataQueryKind{ + Kind: "DataQuery", + Version: "v0", + Group: "prometheus", + Datasource: &dashv2beta1.DashboardV2beta1DataQueryKindDatasource{ + Name: stringPtr("prometheus-uid"), + }, + Spec: map[string]interface{}{ + "expr": "label_values(up, instance)", + }, + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.Len(t, v2alpha1.Spec.Variables, 1) + variable := v2alpha1.Spec.Variables[0] + require.NotNil(t, variable.QueryVariableKind) + assert.Equal(t, "testVar", variable.QueryVariableKind.Spec.Name) + // Verify datasource was moved from query to variable spec + assert.NotNil(t, variable.QueryVariableKind.Spec.Datasource) + assert.Equal(t, "prometheus-uid", *variable.QueryVariableKind.Spec.Datasource.Uid) + assert.Equal(t, "prometheus", *variable.QueryVariableKind.Spec.Datasource.Type) + // Verify query kind was extracted from group + assert.Equal(t, "prometheus", variable.QueryVariableKind.Spec.Query.Kind) + }, + }, + { + name: "empty group in query variable - preserved as empty kind", + createV2beta1: func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2beta1.DashboardVariableKind{ + { + QueryVariableKind: &dashv2beta1.DashboardQueryVariableKind{ + Kind: "QueryVariable", + Spec: dashv2beta1.DashboardQueryVariableSpec{ + Name: "testVar", + Query: dashv2beta1.DashboardDataQueryKind{ + Kind: "DataQuery", + Version: "v0", + Group: "", // Empty group + Spec: map[string]interface{}{ + "expr": "label_values(up, instance)", + }, + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.Len(t, v2alpha1.Spec.Variables, 1) + variable := v2alpha1.Spec.Variables[0] + require.NotNil(t, variable.QueryVariableKind) + assert.Equal(t, "", variable.QueryVariableKind.Spec.Query.Kind, "Empty group should result in empty kind") + }, + }, + { + name: "dashboard with rows layout", + createV2beta1: func() *dashv2beta1.Dashboard { + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Elements: map[string]dashv2beta1.DashboardElement{ + "panel-1": { + PanelKind: &dashv2beta1.DashboardPanelKind{ + Kind: "Panel", + Spec: dashv2beta1.DashboardPanelSpec{ + Id: 1, + Title: "Panel 1", + }, + }, + }, + }, + Layout: dashv2beta1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + RowsLayoutKind: &dashv2beta1.DashboardRowsLayoutKind{ + Kind: "RowsLayout", + Spec: dashv2beta1.DashboardRowsLayoutSpec{ + Rows: []dashv2beta1.DashboardRowsLayoutRowKind{ + { + Kind: "RowsLayoutRow", + Spec: dashv2beta1.DashboardRowsLayoutRowSpec{ + Title: stringPtr("Row 1"), + Collapse: boolPtr(false), + Layout: dashv2beta1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind{ + GridLayoutKind: &dashv2beta1.DashboardGridLayoutKind{ + Kind: "GridLayout", + Spec: dashv2beta1.DashboardGridLayoutSpec{ + Items: []dashv2beta1.DashboardGridLayoutItemKind{ + { + Kind: "GridLayoutItem", + Spec: dashv2beta1.DashboardGridLayoutItemSpec{ + Element: dashv2beta1.DashboardElementReference{ + Kind: "ElementReference", + Name: "panel-1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.NotNil(t, v2alpha1.Spec.Layout.RowsLayoutKind) + require.Len(t, v2alpha1.Spec.Layout.RowsLayoutKind.Spec.Rows, 1) + row := v2alpha1.Spec.Layout.RowsLayoutKind.Spec.Rows[0] + assert.Equal(t, "Row 1", *row.Spec.Title) + assert.False(t, *row.Spec.Collapse) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create v2beta1 dashboard + v2beta1 := tc.createV2beta1() + + // Collect original statistics + originalStats := collectStatsV2beta1(v2beta1.Spec) + + // Convert to v2alpha1 + var v2alpha1 dashv2alpha1.Dashboard + err := scheme.Convert(v2beta1, &v2alpha1, nil) + require.NoError(t, err, "Failed to convert v2beta1 to v2alpha1") + + // Collect v2alpha1 statistics + v2alpha1Stats := collectStatsV2alpha1(v2alpha1.Spec) + + // Verify no data loss + err = detectConversionDataLoss(originalStats, v2alpha1Stats, "V2beta1", "V2alpha1") + assert.NoError(t, err, "Data loss detected in conversion") + + // Run custom validation + tc.validateV2alpha1(t, &v2alpha1) + }) + } +} + +// Helper function to create string pointer +func stringPtr(s string) *string { + return &s +} + +// Helper function to create bool pointer +func boolPtr(b bool) *bool { + return &b +} diff --git a/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json b/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json index df3a62e6dae..d8fc5361f11 100644 --- a/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json +++ b/apps/dashboard/pkg/migration/testdata/input/v15.no-op-migration.json @@ -32,7 +32,9 @@ { "name": "server", "type": "query", - "datasource": "prometheus", + "datasource": { + "type": "prometheus" + }, "query": "label_values(server)", "refresh": 1, "options": [] diff --git a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json index e9eaff02aa7..065fe792748 100644 --- a/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json +++ b/apps/dashboard/pkg/migration/testdata/output/latest_version/v15.no-op-migration.v42.json @@ -89,7 +89,9 @@ "templating": { "list": [ { - "datasource": "prometheus", + "datasource": { + "type": "prometheus" + }, "name": "server", "options": [], "query": "label_values(server)", diff --git a/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json b/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json index d66d7d9aed4..bb4b0b51341 100644 --- a/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json +++ b/apps/dashboard/pkg/migration/testdata/output/single_version/v15.no-op-migration.v15.json @@ -58,7 +58,9 @@ "templating": { "list": [ { - "datasource": "prometheus", + "datasource": { + "type": "prometheus" + }, "name": "server", "options": [], "query": "label_values(server)", diff --git a/apps/example/go.mod b/apps/example/go.mod index 143b915615a..acb714107b3 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -35,7 +35,7 @@ require ( github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -56,7 +56,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/example/go.sum b/apps/example/go.sum index 2fe30a1cd49..00f1a6a2620 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -52,8 +52,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= @@ -116,8 +116,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 659e4e8a019..1c9249745a9 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -40,7 +40,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 56c5afd751f..944e0d69721 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -85,8 +85,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 8db0429adcd..adb3755c58f 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -46,7 +46,7 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 @@ -231,12 +231,12 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grafana/grafana-aws-sdk v1.3.0 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.281.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.283.0 // indirect github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect github.com/grafana/grafana/apps/plugins v0.0.0 // indirect github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect @@ -251,7 +251,7 @@ require ( github.com/grafana/sqlds/v4 v4.2.7 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/consul/api v1.31.2 // indirect @@ -349,7 +349,7 @@ require ( github.com/pressly/goose/v3 v3.25.0 // indirect github.com/prometheus/alertmanager v0.28.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.1 // indirect + github.com/prometheus/common v0.67.2 // indirect github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/exporter-toolkit v0.14.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect @@ -425,18 +425,18 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect gocloud.dev v0.43.0 // indirect - golang.org/x/crypto v0.43.0 // indirect + golang.org/x/crypto v0.44.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 31148a98766..b23eb7f0e4d 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -839,6 +839,8 @@ github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZ github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= @@ -853,6 +855,7 @@ github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxv github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-app-sdk v0.48.1 h1:bKJadWH18WCpJ+Zk8AezRFXCcZgGredRv+fRS+8zkek= github.com/grafana/grafana-app-sdk v0.48.1/go.mod h1:5LljCz+wvmGfkQ8ZKTOfserhtXNEF0cSFthoWShvN6c= +github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= @@ -867,6 +870,7 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.281.0 h1:V8dGyatzcOLQeivFhBV2JWMwTSZH/clDnpfKG9p3dTA= github.com/grafana/grafana-plugin-sdk-go v0.281.0/go.mod h1:3I0g+v6jAwVmrt6BEjDUP4V6pkhGP5QKY5NkXY4Ayr4= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -901,6 +905,7 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= @@ -1374,6 +1379,7 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9 github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= @@ -1730,6 +1736,7 @@ golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1773,6 +1780,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1829,6 +1837,7 @@ golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1853,6 +1862,7 @@ golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1867,6 +1877,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1961,12 +1972,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1978,6 +1992,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2046,6 +2061,7 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2305,20 +2321,27 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= +k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w= k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= +k8s.io/kms v0.34.2/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= k8s.io/kube-aggregator v0.34.1 h1:WNLV0dVNoFKmuyvdWLd92iDSyD/TSTjqwaPj0U9XAEU= k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 51e8ea0c47b..d4f3685a2aa 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -50,7 +50,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index 2056bb1f8f5..712d99405d0 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -51,7 +51,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 246be5289c2..8a254e980d1 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -51,7 +51,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 26bd55d3b5d..7eadc30e628 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -104,8 +104,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index ca743f0f56a..b24c9260327 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -37,7 +37,7 @@ require ( github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -58,7 +58,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 96d8dce605d..b769fbe3916 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -52,8 +52,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= @@ -116,8 +116,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index 51e94063e22..cd702672de6 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -5,7 +5,6 @@ go 1.25.3 require ( github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 - github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -41,9 +40,10 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 02827e1769d..8d576bfaa8b 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -85,8 +85,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/preferences/kinds/manifest.cue b/apps/preferences/kinds/manifest.cue index aa75aa2d0ed..f7d08b81494 100644 --- a/apps/preferences/kinds/manifest.cue +++ b/apps/preferences/kinds/manifest.cue @@ -10,8 +10,7 @@ manifest: { go: {enabled: true} } kinds: [ - preferencesV1alpha1, - starsV1alpha1, + preferencesV1alpha1 ] } } diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/register.go b/apps/preferences/pkg/apis/preferences/v1alpha1/register.go index 6bedc78a325..4de18c09ef6 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/register.go +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/register.go @@ -32,28 +32,6 @@ var PreferencesResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion, }, ) -var StarsResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion, - "stars", "stars", "Stars", - func() runtime.Object { return &Stars{} }, - func() runtime.Object { return &StarsList{} }, - utils.TableColumns{ - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Created At", Type: "date"}, - }, - Reader: func(obj any) ([]any, error) { - p, ok := obj.(*Stars) - if ok && p != nil { - return []any{ - p.Name, - p.CreationTimestamp.UTC().Format(time.RFC3339), - }, nil - } - return nil, fmt.Errorf("expected stars") - }, - }, -) - var ( SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder @@ -70,8 +48,6 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(schemeGroupVersion, &Preferences{}, &PreferencesList{}, - &Stars{}, - &StarsList{}, ) metav1.AddToGroupVersion(scheme, schemeGroupVersion) return nil diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go index c8700a3dfa5..542d5918626 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi.go @@ -20,10 +20,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesNavbarPreference(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference": schema_pkg_apis_preferences_v1alpha1_PreferencesQueryHistoryPreference(ref), "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesSpec": schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars": schema_pkg_apis_preferences_v1alpha1_Stars(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsList": schema_pkg_apis_preferences_v1alpha1_StarsList(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource": schema_pkg_apis_preferences_v1alpha1_StarsResource(ref), - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec": schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref), } } @@ -266,168 +262,3 @@ func schema_pkg_apis_preferences_v1alpha1_PreferencesSpec(ref common.ReferenceCa "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesCookiePreferences", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesNavbarPreference", "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.PreferencesQueryHistoryPreference"}, } } - -func schema_pkg_apis_preferences_v1alpha1_Stars(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{ - Description: "Spec is the spec of the Stars", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec"), - }, - }, - }, - Required: []string{"metadata", "spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_preferences_v1alpha1_StarsList(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/apps/preferences/pkg/apis/preferences/v1alpha1.Stars"), - }, - }, - }, - }, - }, - }, - Required: []string{"metadata", "items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.Stars", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_preferences_v1alpha1_StarsResource(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "group": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "kind": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "names": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "set", - }, - }, - SchemaProps: spec.SchemaProps{ - Description: "The set of resources", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - }, - Required: []string{"group", "kind", "names"}, - }, - }, - } -} - -func schema_pkg_apis_preferences_v1alpha1_StarsSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "resource": { - 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/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"), - }, - }, - }, - }, - }, - }, - Required: []string{"resource"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1.StarsResource"}, - } -} diff --git a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi_violation_exceptions.list b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi_violation_exceptions.list index 80e3e004de7..efec2bc2b3e 100644 --- a/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/preferences/pkg/apis/preferences/v1alpha1/zz_generated.openapi_violation_exceptions.list @@ -1,4 +1,2 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesNavbarPreference,BookmarkUrls -API rule violation: list_type_missing,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsSpec,Resource API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,PreferencesList,ListMeta -API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1,StarsList,ListMeta diff --git a/apps/preferences/pkg/apis/preferences_manifest.go b/apps/preferences/pkg/apis/preferences_manifest.go index dbbbc8d4ab0..d67141364d1 100644 --- a/apps/preferences/pkg/apis/preferences_manifest.go +++ b/apps/preferences/pkg/apis/preferences_manifest.go @@ -23,9 +23,6 @@ var ( rawSchemaPreferencesv1alpha1 = []byte(`{"CookiePreferences":{"additionalProperties":false,"properties":{"analytics":{"additionalProperties":{},"type":"object"},"functional":{"additionalProperties":{},"type":"object"},"performance":{"additionalProperties":{},"type":"object"}},"type":"object"},"NavbarPreference":{"additionalProperties":false,"properties":{"bookmarkUrls":{"items":{"type":"string"},"type":"array"}},"required":["bookmarkUrls"],"type":"object"},"Preferences":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"QueryHistoryPreference":{"additionalProperties":false,"properties":{"homeTab":{"description":"one of: '' | 'query' | 'starred';","type":"string"}},"type":"object"},"spec":{"additionalProperties":false,"properties":{"cookiePreferences":{"$ref":"#/components/schemas/CookiePreferences","description":"Cookie preferences"},"homeDashboardUID":{"description":"UID for the home dashboard","type":"string"},"language":{"description":"Selected language (beta)","type":"string"},"navbar":{"$ref":"#/components/schemas/NavbarPreference","description":"Navigation preferences"},"queryHistory":{"$ref":"#/components/schemas/QueryHistoryPreference","description":"Explore query history preferences"},"regionalFormat":{"description":"Selected locale (beta)","type":"string"},"theme":{"description":"light, dark, empty is default","type":"string"},"timezone":{"description":"The timezone selection\nTODO: this should use the timezone defined in common","type":"string"},"weekStart":{"description":"day of the week (sunday, monday, etc)","type":"string"}},"type":"object"}}`) versionSchemaPreferencesv1alpha1 app.VersionSchema _ = json.Unmarshal(rawSchemaPreferencesv1alpha1, &versionSchemaPreferencesv1alpha1) - rawSchemaStarsv1alpha1 = []byte(`{"Resource":{"additionalProperties":false,"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"Stars":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"resource":{"items":{"$ref":"#/components/schemas/Resource"},"type":"array"}},"required":["resource"],"type":"object"}}`) - versionSchemaStarsv1alpha1 app.VersionSchema - _ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1) ) var appManifestData = app.ManifestData{ @@ -52,22 +49,6 @@ var appManifestData = app.ManifestData{ }, Schema: &versionSchemaPreferencesv1alpha1, }, - - { - Kind: "Stars", - Plural: "Stars", - Scope: "Namespaced", - Conversion: false, - Admission: &app.AdmissionCapabilities{ - Validation: &app.ValidationCapability{ - Operations: []app.AdmissionOperation{ - app.AdmissionOperationCreate, - app.AdmissionOperationUpdate, - }, - }, - }, - Schema: &versionSchemaStarsv1alpha1, - }, }, Routes: app.ManifestVersionRoutes{ Namespaced: map[string]spec3.PathProps{}, @@ -88,7 +69,6 @@ func RemoteManifest() app.Manifest { var kindVersionToGoType = map[string]resource.Kind{ "Preferences/v1alpha1": v1alpha1.PreferencesKind(), - "Stars/v1alpha1": v1alpha1.StarsKind(), } // ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 5a00cb7031b..9bb12d5ae4f 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -42,7 +42,7 @@ require ( github.com/google/go-github/v64 v64.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grafana/grafana-app-sdk v0.48.2 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -57,7 +57,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index 6a63c2cd85e..78f06781b15 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -58,8 +58,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= @@ -113,8 +113,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/secret/go.mod b/apps/secret/go.mod index c64f6ad2a2e..2ac03ea3f8b 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -6,9 +6,9 @@ require ( github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 + go.yaml.in/yaml/v3 v3.0.4 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 - gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 @@ -45,7 +45,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect @@ -54,7 +54,6 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sys v0.38.0 // indirect @@ -63,6 +62,7 @@ require ( golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 29563e7984d..74024f8df34 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -89,8 +89,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go index a8738c3093a..0a3d230f50a 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go +++ b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go @@ -9,7 +9,7 @@ import ( "fmt" "strconv" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) const redacted = "[REDACTED]" diff --git a/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go index 90d53bdf7ae..d9bede562f8 100644 --- a/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go +++ b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) func TestExposedSecureValue(t *testing.T) { diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 7f853adb219..382cf8dd051 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -35,7 +35,7 @@ require ( github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // indirect + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -56,7 +56,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index a19635cc757..e31288da121 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -52,8 +52,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= @@ -116,8 +116,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= diff --git a/conf/defaults.ini b/conf/defaults.ini index 620e009fe92..125c748326c 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -581,10 +581,6 @@ external_manage_info = # Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. viewers_can_edit = false -# Deprecated: Assign your editors to admins. -# Editors can administrate dashboard, folders and teams they create -editors_can_admin = false - # The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 24h (24 hours). The minimum supported duration is 15m (15 minutes). user_invite_max_lifetime_duration = 24h @@ -1963,6 +1959,10 @@ max_connections = 100 # The limit can be disabled by setting it to -1. message_size_limit = 8388608 +# client_queue_max_size is the maximum size in bytes of the client queue +# for Live connections. Defaults to 4MB. +client_queue_max_size = 4194304 + # allowed_origins is a comma-separated list of origins that can establish connection with Grafana Live. # If not set then origin will be matched over root_url. Supports wildcard symbol "*". allowed_origins = diff --git a/conf/sample.ini b/conf/sample.ini index 6d41d750b8a..3944ea20940 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -568,10 +568,6 @@ # Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. ;viewers_can_edit = false -# Deprecated: Assign your editors to admins. -# Editors can administrate dashboard, folders and teams they create -;editors_can_admin = false - # The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 24h (24 hours). The minimum supported duration is 15m (15 minutes). ;user_invite_max_lifetime_duration = 24h @@ -1907,6 +1903,10 @@ default_datasource_uid = # ha_prefix is a prefix for keys in the HA engine. It's used to separate keys for different Grafana instances. ;ha_prefix = +# client_queue_max_size is the maximum size in bytes of the client queue +# for Live connections. Defaults to 4MB. +;client_queue_max_size = + #################################### Grafana Image Renderer Plugin ########################## [plugin.grafana-image-renderer] # Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert. diff --git a/contribute/feature-toggles.md b/contribute/feature-toggles.md index 1e6b1b1b885..aa74b2d7fc2 100644 --- a/contribute/feature-toggles.md +++ b/contribute/feature-toggles.md @@ -2,6 +2,8 @@ This guide helps you to add your feature behind a _feature flag_, code that lets you enable or disable a feature without redeploying Grafana. +Exhaustive documentation on OpenFeature can be found at [OpenFeature.dev](https://openfeature.dev/) + ## Steps to adding a feature toggle 1. Define the feature toggle in [registry.go](../pkg/services/featuremgmt/registry.go). To see what each feature stage means, look at the [related comments](../pkg/services/featuremgmt/features.go). If you are a community member, use the [CODEOWNERS](../.github/CODEOWNERS) file to determine which team owns the package you are updating. @@ -13,7 +15,68 @@ Once your feature toggle is defined, you can then wrap your feature around a che Examples: -- [Backend](https://github.com/grafana/grafana/blob/feb2b5878b3e3ec551d64872c35edec2a0187812/pkg/services/authn/clients/session.go#L57): Use the `IsEnabled` function and pass in your feature toggle. +### Backend + +Use the OpenFeature client for all new backend feature flags. + +#### Key points: + +- OpenFeature SDK relies on the global state. +- OpenFeature Provider configuration happens in the `commands` module before initialization of other modules. +- It is safe to create an instance of the OpenFeature client directly in a function. +- Flag evaluation is context-aware -- you can pass metadata such as org name, grafana version, or environment to the evaluation context. +- Always perform flag evaluation at runtime, not during service startup, to ensure correct and up-to-date flag values. +- Do not cache or store flag values globally -- evaluate flags when needed, especially in request or handler logic. + +#### In Grafana code: + +```go +import "github.com/open-feature/go-sdk/openfeature" + +client := openfeature.NewDefaultClient() + +if client.Boolean(ctx, MyTestFlag, false, openfeature.TransactionContext(ctx)) { + ... +} +``` + +#### In Tests: + +```go +import ( + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/testing" +) + +var ( + // Since openfeature relies on global state, + // TestProvider should be a global instance shared between tests + // Under the hood it uses a custom _goroutine local_ storage to manage state on a per-test basis + provider = testing.NewTestProvider() +) + +func TestMain(m *testing.M) { + fmt.Println("Setting up test environment...") + + if err := openfeature.SetProvider(provider); err != nil { + ... + } + + exitCode := m.Run() + os.Exit(exitCode) +} + +func TestFoo(t *testing.T) { + t.Parallel() + + testFlags := map[string]memprovider.InMemoryFlag{ + ... + } + + provider.UsingFlags(t, testFlags) + ... +} +``` ### Frontend diff --git a/devenv/scopes/scopes.go b/devenv/scopes/scopes.go index 9e712aedc4d..1f8a698ccda 100644 --- a/devenv/scopes/scopes.go +++ b/devenv/scopes/scopes.go @@ -13,7 +13,7 @@ import ( "os" "strings" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1" diff --git a/docs/sources/as-code/infrastructure-as-code/_index.md b/docs/sources/as-code/infrastructure-as-code/_index.md index 1579602467b..c976df81106 100644 --- a/docs/sources/as-code/infrastructure-as-code/_index.md +++ b/docs/sources/as-code/infrastructure-as-code/_index.md @@ -4,17 +4,23 @@ keywords: - Quickstart - Grafana Cloud menuTitle: Infrastructure as code -title: Provision Grafana Cloud with infrastructure as code +title: Provision Grafana Cloud with Infrastructure as code weight: 800 +labels: + products: + - cloud canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ --- -# Provision Grafana Cloud with infrastructure as code +# Provision Grafana Cloud with Infrastructure as code -With Grafana Cloud, you can create dashboards via configuration files in source code. This enables you to review code, reuse it, and create better workflows. +With Grafana Cloud, you can use as-code tools to create and manage resources via code, and incorporate them efficiently into your own use cases. This enables you to review code, reuse it, and create better workflows. -Via code, you can _declaratively_ manage _what_ Grafana resources to use. -The as-code tools and tutorials that follow show you what do to, to declaratively manage Grafana resources, and incorporate them efficiently into your own use cases. +{{< admonition type="note" >}} + +Most of the tools defined here can be used with one another. + +{{< /admonition >}} ## Grafana Terraform provider @@ -67,7 +73,7 @@ To get started, see the [quickstart guides for the Grafana Ansible Collection](/ ### Who is this recommended for? -Like Terraform, the Grafana Ansible collection is best suited for people already using Ansible for non-Grafana use cases. The collection only works for Grafana Cloud right now, so it makes the most sense for Grafana Cloud customers who want to manage resources declaratively using Ansible. +Like Terraform, the Grafana Ansible collection is best suited for people already using Ansible for non-Grafana use cases. The collection only works for Grafana Cloud right now, so it makes the most sense for Grafana Cloud customers who want to manage resources using Ansible. ### Known limitations @@ -75,7 +81,7 @@ The Grafana Ansible collection only works for Grafana Cloud and only supports ei ## Grafana Operator -The Grafana Operator is a Kubernetes operator that can provision, manage, and operate Grafana instances and their associated resources within Kubernetes through Custom Resources. This Kubernetes-native tool eases the administration of Grafana, offering a declarative approach to managing dashboards, data sources, and folders. It also automatically syncs the Kubernetes Custom resources and the actual resources in the Grafana Instance. It supports leveraging Grafonnet for generating Grafana dashboard definitions for seamless dashboard configuration as code. +The Grafana Operator is a Kubernetes operator that can provision, manage, and operate Grafana instances and their associated resources within Kubernetes through Custom Resources. This Kubernetes-native tool eases the administration of Grafana, including managing dashboards, data sources, and folders. It also automatically syncs the Kubernetes Custom resources and the actual resources in the Grafana Instance. It supports leveraging Grafonnet for generating Grafana dashboard definitions for seamless dashboard configuration as code. To get started, see the [quickstart guides for the Grafana Operator](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/) or check out the [Grafana Operator's documentation](https://grafana.github.io/grafana-operator/). @@ -120,7 +126,7 @@ To get started with the Grafana Crossplane provider, install Crossplane in the K kubectl crossplane install provider grafana/crossplane-provider-grafana:v0.1.0 ``` -During installation of the provider, CRDs for all the resources supported by the Terraform provider are added to the cluster so users can begin defining their Grafana resources as Kubernetes custom resources. The Crossplane provider ensures that whatever is defined in the custom resource definitions is what is visible in Grafana UI. If any changes are made directly in the UI, the changes will be discarded when the provider resyncs. This helps ensure that whatever is defined declaratively in the cluster will be the source of truth for Grafana resources. +During installation of the provider, CRDs for all the resources supported by the Terraform provider are added to the cluster so users can begin defining their Grafana resources as Kubernetes custom resources. The Crossplane provider ensures that whatever is defined in the custom resource definitions is what is visible in Grafana UI. If any changes are made directly in the UI, the changes will be discarded when the provider resyncs. This helps ensure that whatever is defined via code in the cluster will be the source of truth for Grafana resources. To get started, refer to the examples folder in the Grafana Crossplane repository. @@ -152,16 +158,15 @@ To use the Crossplane provider, you must have the Crossplane CLI and Crossplane ## Grafana as code comparison -Most of the tools defined here can be used with one another. The following chart compares the properties and tools mentioned above. -| Property/Tool | Grafana Terraform Provider | Grafana Ansible Collection | Grafana Operator | Grizzly | Grafana Crossplane Provider | -| -------------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| Grafana resources supported | All major Grafana resources | Grafana Cloud stack, plugins, API keys, dashboards, data sources, and folders | Dashboards, Datasources, Folders | Synthetic Monitoring checks, dashboards, data sources, folders, and Prometheus rules | All major Grafana resources | -| Tool format | HCL/JSON | YAML | YAML | Jsonnet/YAML/JSON | YAML/JSON | -| Follows Kubernetes-style manifests | | | ✓ | ✓ | ✓ | -| Easy dashboard building process | | | ✓ | ✓ | | -| Manage resources using Kubernetes | | | ✓ | | ✓ | -| Retrieves Grafana resource information | ✓ | | | | | -| Built-in resource sync process | | | ✓ | ✓ | ✓ | -| Recommended for | Existing Terraform users | Existing Ansible users | Users looking to manage Grafana resources from within Kubernetes | Users looking to define Grafana resources in a Kubernetes-style YAML and users looking to get built-in workflow support and sync process | Users looking to manage Grafana resources from within Kubernetes | +| Property/Tool | Grafana Terraform Provider | Grafana Ansible Collection | Grafana Operator | Grafana Crossplane Provider | +| -------------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | +| Grafana resources supported | All major Grafana resources | Grafana Cloud stack, plugins, API keys, dashboards, data sources, and folders | Dashboards, data sources, Folders | All major Grafana resources | +| Tool format | HCL/JSON | YAML | YAML | YAML/JSON | +| Follows Kubernetes-style manifests | | | ✓ | ✓ | +| Easy dashboard building process | | | ✓ | | +| Manage resources using Kubernetes | | | ✓ | ✓ | +| Retrieves Grafana resource information | ✓ | | | | +| Built-in resource sync process | | | ✓ | ✓ | +| Recommended for | Existing Terraform users | Existing Ansible users | Users looking to manage Grafana resources from within Kubernetes | Users looking to manage Grafana resources from within Kubernetes | diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/_index.md b/docs/sources/as-code/infrastructure-as-code/ansible/_index.md index 46d44ff1f90..2f977b0166e 100644 --- a/docs/sources/as-code/infrastructure-as-code/ansible/_index.md +++ b/docs/sources/as-code/infrastructure-as-code/ansible/_index.md @@ -8,22 +8,42 @@ menuTitle: Ansible title: Grafana Ansible collection weight: 110 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ +aliases: + - ../../infrastructure-as-code/ansible/ansible-grafana-agent-linux + - ../../infrastructure-as-code/ansible/ansible-multiple-agents +labels: + products: + - cloud --- # Grafana Ansible collection -The [Grafana Ansible collection](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) provides configuration management resources for Grafana. You can use it to manage resources such as dashboards, Cloud stacks, folders, and more. +The [Grafana Ansible collection](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) provides configuration management resources for Grafana. You can use it to manage: -The collection also houses the [Grafana Agent role](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent) which can be used to deploy and manage Grafana Agent across various Linux machines. +- Grafana Cloud stacks +- Dashboards +- Data sources +- Folders +- Alerting contact points +- Notification policies +- API keys + +If your resources aren't currently available in the Grafana Ansible collection, you can manage them on Grafana Cloud programmatically by writing Ansible playbooks that use the [Ansible's built-in URI module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html) to call the [HTTP APIs](/docs/grafana/latest/developers/http_api/) to manage resources for the Grafana Cloud portal, as well as those within a stack. + +## Learn more + +Refer to [Create and manage a Grafana Cloud stack using Ansible](ansible-cloud-stack/) to learn how to create a Grafana Cloud stack and add a data source and dashboard using [Ansible](https://www.ansible.com/). + +To learn more about managing Grafana with Infrastructure as code: + +- [Grafana Ansible collection documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) +- [Ansible playbook best practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html) +- [Grafana API documentation](/docs/grafana/latest/developers/http_api/) +- [Grafana Cloud API documentation](https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/) +- [Infrastructure as Code with Terraform](/docs/grafana/latest/as-code/infrastructure-as-code/terraform/) + +## Grafana Agent (deprecated) {{< docs/shared lookup="agent-deprecation.md" source="alloy" version="next" >}} -For resources currently not available in the Grafana Ansible collection, you can manage those resources on Grafana Cloud programmatically by writing Ansible playbooks that use the [Ansible's builtin uri module](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uri_module.html) to call the [HTTP APIs](/docs/grafana/latest/developers/http_api/) to manage resources for the Grafana Cloud portal, as well as those within a stack. - -Use the following guides to get started using Ansible to manage your Grafana Cloud stack: - -| Topic | Description | -| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| [Create and manage a Grafana Cloud stack using Ansible](ansible-cloud-stack/) | Describes how to create a Grafana Cloud stack and add a data source and dashboard using [Ansible](https://www.ansible.com/). | -| [Install Grafana Agent on a Linux host using Ansible](ansible-grafana-agent-linux/) | Describes how to install the Grafana Agent on a Linux node using Ansible and use it to push logs to Grafana Cloud. | -| [Monitor multiple Linux hosts with Grafana Agent Role](ansible-multiple-agents/) | Describes how to use the Grafana Ansible collection to manage agents across multiple Linux hosts. | +The Ansible collection also houses [Grafana Agent role](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent), which is now deprecated. diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md index 5dcab277f04..55f875fd1a7 100644 --- a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md +++ b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/index.md @@ -4,64 +4,78 @@ keywords: - Quickstart - Grafana Cloud - Ansible -title: Create and manage a Grafana Cloud stack using Ansible +title: Create and manage your Grafana Cloud stack using Ansible +menuTitle: Manage stack using Ansible weight: 100 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ansible-cloud-stack/ --- -# Create and manage a Grafana Cloud stack using Ansible +# Create and manage your Grafana Cloud stack using Ansible -Learn how to add a data source, a dashboard, and a folder to a Grafana Cloud stack using Ansible collection for Grafana. +This guide shows you how to create a Grafana Cloud stack and add a data source, dashboard, and folder using the Ansible Collection for Grafana. You'll manage your Grafana infrastructure through Ansible playbooks. ## Before you begin -Before you begin, you should have the following available: +Before you begin, make sure you have the following available: -- A Grafana Cloud account. +- A Grafana Cloud account - [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/index.html) installed on your machine +## Install the Grafana Ansible collection + +Install the Grafana Ansible collection: + +```sh +ansible-galaxy collection install grafana.grafana +``` + +This collection provides all the modules needed to manage Grafana Cloud stacks and resources. + ## Create a Cloud stack -1. Create a Grafana Cloud Access Policy and get a token. - You'll need this for the Ansible playbook to be able to create a Grafana Cloud stack. - Refer to [Create a Grafana Cloud Access Policy](/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-policies/create-access-policies/). +First, create a Grafana Cloud Access Policy and get a token. You'll need this for the Ansible playbook to be able to create a Grafana Cloud stack. Refer to [Create a Grafana Cloud Access Policy](/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-policies/create-access-policies/). -1. Create an Ansible playbook file. +Next, create an Ansible playbook file. This Ansible playbook creates a Grafana Cloud stack using the [Cloud stack module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/cloud_stack_module.html#ansible-collections-grafana-grafana-cloud-stack-module). - This Ansible playbook will create a Grafana Cloud stack by using the [Cloud stack module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/cloud_stack_module.html#ansible-collections-grafana-grafana-cloud-stack-module). +To do so, create a file named `cloud-stack.yml` and add the following: - Create a file named `cloud-stack.yml` and add the following: +```yaml +- name: Create Grafana Cloud stack + connection: local + hosts: localhost - ```yaml - - name: Create Grafana Cloud stack - connection: local - hosts: localhost + vars: + grafana_cloud_api_key: '' + stack_name: '' + org_name: '' - vars: - grafana_cloud_api_key: '' - stack_name: '' - org_name: '' + tasks: + - name: Create a Grafana Cloud stack + grafana.grafana.cloud_stack: + name: '{{ stack_name }}' + stack_slug: '{{ stack_name }}' + cloud_api_key: '{{ grafana_cloud_api_key }}' + org_slug: '{{ org_name }}' + delete_protection: true + state: present + register: stack_result - tasks: - - name: Create a Grafana Cloud stack - grafana.grafana.cloud_stack: - name: '{{ stack_name }}' - stack_slug: '{{ stack_name }}' - cloud_api_key: '{{ grafana_cloud_api_key }}' - org_slug: '{{ org_name }}' - delete_protection: true - state: present - ``` + - name: Display stack URL + debug: + msg: 'Stack created at: {{ stack_result.url }}' +``` -1. Replace the following field values: - - `` with a token from the Cloud Access Policy you created in the Grafana Cloud portal. - - `` with the name of your stack. - - `` with the name of the organization in Grafana Cloud. +Replace the placeholders with your values: -## Create an API key in the Grafana stack +- _``_: Token from the Cloud Access Policy you created in the Grafana Cloud portal +- _``_: Name of your stack +- _``_: Name of the organization in Grafana Cloud -Create an API key in the Grafana stack. -You'll need this key to configure Ansible to be able to create data source, folders, and dashboards. +The playbook registers the stack creation result and displays the stack URL, which you'll need for subsequent resource management. + +## Create an API key in your Grafana stack + +Create an API key in the Grafana stack. You'll need this key to configure Ansible to create data sources, folders, and dashboards. 1. Log into your Grafana Cloud instance. 2. Click **Administration** and select **API keys**. @@ -70,171 +84,169 @@ You'll need this key to configure Ansible to be able to create data source, fold 5. In **Role**, select **Admin** or **Editor** to associate the role with this API key. 6. Click **Copy** to save it for later use. -## Add a data source +## Add resources using playbooks -This guide uses the InfluxDB data source. -The required arguments vary depending on the type of data source you select. +### Add a data source -1. Create a file named `data-source.yml` and add the following: +The following steps use the InfluxDB data source. The required arguments vary depending on the type of data source you select. - ```yaml - - name: Add/Update data source - connection: local - hosts: localhost +Create a file named `data-source.yml`: - vars: - data_sources: - [ - { - name: '', - type: 'influxdb', - url: '', - user: '', - secureJsonData: { password: '' }, - database: '', - id: , - uid: '', - access: 'proxy', - }, - ] +```yaml +- name: Add/Update data source + connection: local + hosts: localhost - grafana_api_key: '' - stack_name: '' + vars: + grafana_url: 'https://.grafana.net' + grafana_api_key: '' + data_source_config: + name: '' + type: 'influxdb' + url: '' + user: '' + secureJsonData: + password: '' + database: '' + uid: '' + access: 'proxy' - tasks: - - name: Create/Update Data sources - grafana.grafana.datasource: - datasource: '{{ item }}' - stack_slug: '{{ stack_name }}' - grafana_api_key: '{{ grafana_api_key }}' - state: present - loop: '{{ data_sources }}' - ``` + tasks: + - name: Create/Update Data source + grafana.grafana.datasource: + dataSource: '{{ data_source_config }}' + grafana_url: '{{ grafana_url }}' + grafana_api_key: '{{ grafana_api_key }}' + state: present +``` -1. Replace the following field values: - - `` with the name of the data source to be added in Grafana. - - `` with URL of your data source. - - `` with the username for authenticating with your data source. - - `` with the password for authenticating with your data source. - - `` with name of your database. - - `` with the ID for your data source in Grafana. - - `` wth the UID for your data source in Grafana. - - `` with the name of your stack. - - `` with the [API key created in the Grafana instance](#create-an-api-key-in-the-grafana-stack). +Replace the placeholders with your values: -## Add a folder +- _``_: Name of the data source to be added in Grafana +- _``_: URL of your data source +- _``_: Username for authenticating with your data source +- _``_: Password for authenticating with your data source +- _``_: Name of your database +- _``_: UID for your data source in Grafana +- _``_: Name of your stack +- _``_: API key created in the Grafana instance -This Ansible playbook creates a folder in your Grafana instance by using the [Folder module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/folder_module.html#ansible-collections-grafana-grafana-folder-module). +### Add a folder -1. Create a file named `folder.yml` and add the following: +This playbook creates a folder in your Grafana instance using the [Folder module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/folder_module.html#ansible-collections-grafana-grafana-folder-module). - ```yaml - - name: Add/Update Folders - connection: local - hosts: localhost +Create a file named `folder.yml`: - vars: - folders: [{ title: '', uid: '' }] +```yaml +- name: Add/Update Folders + connection: local + hosts: localhost - stack_name: '' - grafana_api_key: +vars: + grafana_url: 'https://.grafana.net' + grafana_api_key: '' + folders: + - title: '' + uid: '' - tasks: - - name: Create/Update a Folder in Grafana - grafana.grafana.folder: - title: '{{ item.title }}' - uid: '{{ item.uid }}' - stack_slug: '{{ stack_name }}' - grafana_api_key: '{{ grafana_api_key }}' - state: present - loop: '{{ folders }}' - ``` + tasks: + - name: Create/Update a Folder in Grafana + grafana.grafana.folder: + title: '{{ item.title }}' + uid: '{{ item.uid }}' + grafana_url: '{{ grafana_url }}' + grafana_api_key: '{{ grafana_api_key }}' + state: present + loop: '{{ folders }}' +``` -1. Replace the following field values: - - `` with the name of the folder to be added in Grafana. - - `` with the UID for your folder in Grafana. - - `` with the name of your stack. - - `` with the [API key created in the Grafana instance](#create-an-api-key-in-the-grafana-stack). +Replace the placeholders with your values: -## Add a dashboard to the folder +- _``_: Name of the folder to be added in Grafana +- _``_: UID for your folder in Grafana +- _``_: Name of your stack +- _``_: API key created in the Grafana instance -This Ansible playbook iterates through the dashboard JSON source code files in the folder referenced in `dashboards_path` and adds them in the Grafana instance by using the [Dashboard module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/dashboard_module.html#ansible-collections-grafana-grafana-dashboard-module). +### Add a dashboard to the folder -1. Create a file named `dashboard.yml` and add the following: +This playbook iterates through the dashboard JSON source code files in the folder referenced in `dashboards_path` and adds them to the Grafana instance using the [Dashboard module](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/dashboard_module.html#ansible-collections-grafana-grafana-dashboard-module). - ```yaml - - name: Add/Update Dashboards - connection: local - hosts: localhost +Create a file named `dashboard.yml`: - vars: - dashboards_path: # Example "./dashboards" - stack_name: "" - grafana_api_key: +```yaml +- name: Add/Update Dashboards + connection: local + hosts: localhost - tasks: - - name: Find dashboard files - find: - paths: "{{ dashboards_path }}" - file_type: file - recurse: Yes - patterns: "*.json" - register: files_matched - no_log: True + vars: + grafana_url: 'https://.grafana.net' + grafana_api_key: '' + dashboards_path: '' # Example "./dashboards" - - name: Create list of dashboard file names - set_fact: - dashboard_file_names: "{{ dashboard_file_names | default ([]) + [item.path] }}" - loop: "{{ files_matched.files }}" - no_log: True + tasks: + - name: Find dashboard files + find: + paths: '{{ dashboards_path }}' + file_type: file + recurse: true + patterns: '*.json' + register: files_matched + no_log: true - - name: Create/Update a dashboard - grafana.grafana.dashboard: - dashboard: "{{ lookup('ansible.builtin.file','{{ item }}' ) }}" - stack_slug: "{{ stack_name }}" - grafana_api_key: "{{ grafana_api_key }}" - state: present - loop: "{{ dashboard_file_names }}" - ``` + - name: Create list of dashboard file names + set_fact: + dashboard_file_names: '{{ dashboard_file_names | default([]) + [item.path] }}' + loop: '{{ files_matched.files }}' + no_log: true -1. Replace the following field values: - - `` with the path to the folder containing dashboard JSON source code files. - - `` with the name of your stack. - - `` with the [API key created in the Grafana instance](#create-an-api-key-in-the-grafana-stack). + - name: Create/Update a dashboard + grafana.grafana.dashboard: + dashboard: "{{ lookup('ansible.builtin.file', item) }}" + grafana_url: '{{ grafana_url }}' + grafana_api_key: '{{ grafana_api_key }}' + state: present + loop: '{{ dashboard_file_names }}' +``` + +Replace the placeholders with your values: + +- _``_: Path to the folder containing dashboard JSON source code files +- _``_: Name of your stack +- _``_: API key created in the Grafana instance ## Run the Ansible playbooks In a terminal, run the following commands from the directory where all of the Ansible playbooks are located. -1. To create the Grafana Cloud stack. +Create the Grafana Cloud stack: - ```shell - ansible-playbook cloud-stack.yml - ``` +```sh +ansible-playbook cloud-stack.yml +``` -1. To add a data source to the Grafana stack. +Add a data source to the Grafana stack: - ```shell - ansible-playbook data-source.yml - ``` +```sh +ansible-playbook data-source.yml +``` -1. To add a folder to the Grafana stack +Add a folder to the Grafana stack: - ```shell - ansible-playbook folder.yml - ``` +```sh +ansible-playbook folder.yml +``` -1. To add a dashboard to the folder in your Grafana stack. +Add a dashboard to the folder in your Grafana stack: - ```shell - ansible-playbook dashboard.yml - ``` +```sh +ansible-playbook dashboard.yml +``` -## Validation +## Validate your configuration -Once you run the Ansible playbooks, you should be able to verify the following: +After you've run the Ansible playbooks, you can verify the following: -- The new Grafana stack is created and visible in the Cloud Portal. +- The new Grafana Cloud stack is created and visible in the Cloud Portal. ![Cloud Portal](/static/img/docs/grafana-cloud/terraform/cloud_portal_tf.png) @@ -242,18 +254,22 @@ Once you run the Ansible playbooks, you should be able to verify the following: ![InfluxDB datasource](/media/docs/grafana-cloud/screenshot-influxdb_datasource_tf.png) -- A new folder in Grafana. - In the following image, a folder named `Demos` was added. +- A new folder is available in your Grafana stack. In the following image, a folder named `Demos` was added. ![Folder](/media/docs/grafana-cloud/screenshot-folder_tf.png) -- A new dashboard in the Grafana stack. - In the following image a dashboard named `InfluxDB Cloud Demos` was created inside the "Demos" folder. +- A new dashboard is visible in the Grafana stack. In the following image, a dashboard named `InfluxDB Cloud Demos` was created inside the "Demos" folder. ![InfluxDB dashboard](/static/img/docs/grafana-cloud/terraform/influxdb_dashboard_tf.png) -## Summary +## Next steps -In this guide, you created a Grafana Cloud stack along with a data source, folder, and dashboard imported from a JSON file using Ansible. +You've successfully created a Grafana Cloud stack along with a data source, a folder, and a dashboard using Ansible. Your Grafana infrastructure is now managed through code. -To learn more about managing Grafana using Ansible, refer to the [Grafana Ansible collection](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/). +To learn more about managing Grafana with Infrastructure as code: + +- [Grafana Ansible collection documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/) +- [Ansible playbook best practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html) +- [Grafana API documentation](/docs/grafana/latest/developers/http_api/) +- [Grafana Cloud API documentation](https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/) +- [Infrastructure as Code with Terraform](/docs/grafana/latest/as-code/infrastructure-as-code/terraform/) diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/index.md b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/index.md deleted file mode 100644 index 8fa3a6f8357..00000000000 --- a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/index.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -keywords: - - Infrastructure as Code - - Quickstart - - Grafana Cloud - - Ansible -title: Install Grafana Agent on a Linux host using Ansible -weight: 200 -canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ansible-grafana-agent-linux/ ---- - -# Install Grafana Agent on a Linux host using Ansible - -{{< docs/shared lookup="agent-deprecation.md" source="alloy" version="next" >}} - -This guide shows how to install Grafana Agent on a Linux host using [Ansible](https://www.ansible.com/) and to use it to push logs to Grafana Cloud. - -## Before you begin - -Before you begin, you should have the following available: - -- A Grafana Cloud account. -- A Linux machine -- Command line (terminal) access to that Linux machine with `unzip` binary installed -- Account permissions sufficient to install and use Grafana Agent on the Linux machine -- [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/index.html) installed on the Linux machine - -## Choose your Grafana Agent installation method - -This guide covers two methods for installing and configuring Grafana Agent using Ansible: - -- Installing Grafana Agent in Flow mode -- Installing Grafana Agent in static mode - -Depending on your specific needs and the configuration of your environment, you may choose one method over the other for better compatibility or ease of setup. - - - -### Install Grafana Agent in flow mode using Ansible - -This Ansible playbook installs Grafana Agent in Flow mode and also creates a systemd service to manage it. - -It creates a user named `grafana-agent` on the Linux machine for running Grafana Agent. - -1. Create a file named `grafana-agent.yml` and add the following: - -```yaml -- name: Install Grafana Agent Flow - hosts: all - become: true - tasks: - - name: Install Grafana Agent Flow - ansible.builtin.include_role: - name: grafana.grafana.grafana_agent - vars: - grafana_agent_mode: flow - # Change config file on the host to .river - grafana_agent_config_filename: config.river - # Change config file to be copied - grafana_agent_provisioned_config_file: '' - # Remove default flags - grafana_agent_flags_extra: - server.http.listen-addr: '0.0.0.0:12345' -``` - -1. Replace the following field values: - - `` with the path to river configuration file on the Ansible Controller (Localhost). - -### Install Grafana Agent in static mode using Ansible - -This Ansible playbook installs Grafana Agent in static mode and also creates a systemd service to manage it. -It creates a user named `grafana-agent` on the Linux machine for running Grafana Agent. - -1. Create a file named `grafana-agent.yml` and add the following: - -```yaml -- name: Install Grafana Agent in static mode - hosts: all - become: true - - vars: - grafana_cloud_api_key: - logs_username: # Example - 411478 - loki_url: # Example - https://logs-prod-017.grafana.net/loki/api/v1/push - tasks: - - name: Install Grafana Agent in static mode - ansible.builtin.include_role: - name: grafana_agent - vars: - grafana_agent_logs_config: - configs: - - clients: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ logs_username }}' - url: '{{ loki_url }}' - name: default - positions: - filename: /tmp/positions.yaml - scrape_configs: - - job_name: integrations/node_exporter_direct_scrape - static_configs: - - targets: - - localhost - labels: - instance: hostname - __path__: /var/log/*.log - job: integrations/node_exporter - target_config: - sync_period: 10s -``` - -1. Replace the following field values: - - `` with a token from the Cloud Access Policy you created in the Grafana Cloud portal. - - `` with the Loki Username - - `` with the push endpoint URL of Loki Instance - -## Run the Ansible playbook on the Linux machine - -In the Linux machine's terminal, run the following command from the directory where the Ansible playbook is located. - -```shell -ansible-playbook grafana-agent.yml -``` - -## Validate - - - -1. Grafana Agent service on the Linux machine should be `active` and `running`. You should see a similar output: - - -```shell -$ sudo systemctl status grafana-agent.service - grafana-agent.service - Grafana Agent - Loaded: loaded (/etc/systemd/system/grafana-agent.service; enabled; vendor preset: enabled) - Active: active (running) since Wed 2022-07-20 09:56:15 UTC; 36s ago - Main PID: 3176 (agent-linux-amd) - Tasks: 8 (limit: 515) - Memory: 92.5M - CPU: 380ms - CGroup: /system.slice/grafana-agent.service - └─3176 /usr/local/bin/agent-linux-amd64 --config.file=/etc/grafana-cloud/agent-config.yaml -``` - -1. In a Grafana Cloud stack, click **Explore** in the left-side menu. - -1. At the top of the page, use the dropdown menu to select your Loki logs data source. In the Log Browser, run the query `{job="integrations/node_exporter"}` - - ![Loki Logs](/static/img/docs/grafana-cloud/ansible/ansible-agent-logs.png) - -## Summary - -In this guide, you installed Grafana Agent on a Linux node using Ansible and used it to pushed logs to Grafana Cloud. - -To learn more about the Grafana Ansible collection, refer to the [GitHub repository](https://github.com/grafana/grafana-ansible-collection) or its [documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/index.html). diff --git a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/index.md b/docs/sources/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/index.md deleted file mode 100644 index c6769e5ebde..00000000000 --- a/docs/sources/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/index.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -menuTitle: Monitor multiple Linux hosts with the grafana_agent role -title: Monitor multiple Linux hosts with grafana_agent role -weight: 300 -canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/ansible-multiple-agents/ ---- - -# Monitor multiple Linux hosts with the `grafana_agent` role - -{{< docs/shared lookup="agent-deprecation.md" source="alloy" version="next" >}} - -Monitoring multiple Linux hosts can be difficult. -To make it easier, you can use the `grafana_agent` role with the [Grafana Ansible collection](../). -This guide shows how to use the `grafana_agent` Ansible role to deploy and manage Grafana Agent across multiple Linux hosts so you can monitor them using Grafana Cloud. - -## Before you begin - -Before you begin, you should have: - -- Linux hosts -- SSH access to the Linux hosts -- Account permissions sufficient to install and use Grafana Agent on the Linux hosts - -## Install the Grafana Ansible collection - -The [`grafana_agent` role](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent) is available in the Grafana Ansible collection as of the 1.1.0 release. - -To install the Grafana Ansible collection, run this command: - -``` -ansible-galaxy collection install grafana.grafana -``` - -## Create an Ansible inventory file - -Next, you will set up your hosts and create an inventory file. - -1. Create your hosts and add public SSH keys to them. - - This example uses eight Linux hosts: two Ubuntu hosts, two CentOS hosts, two Fedora hosts, and two Debian hosts. - -1. Create an Ansible inventory file. - - The Ansible inventory, which resides in a file named `inventory`, looks similar to this: - - ``` - 146.190.208.216 # hostname = ubuntu-01 - 146.190.208.190 # hostname = ubuntu-02 - 137.184.155.128 # hostname = centos-01 - 146.190.216.129 # hostname = centos-02 - 198.199.82.174 # hostname = debian-01 - 198.199.77.93 # hostname = debian-02 - 143.198.182.156 # hostname = fedora-01 - 143.244.174.246 # hostname = fedora-02 - ``` - -1. Create an `ansible.cfg` file within the same directory as `inventory`, with the following values: - ``` - [defaults] - inventory = inventory # Path to the inventory file - private_key_file = ~/.ssh/id_rsa # Path to my private SSH Key - remote_user=root # username - ``` - {{< admonition type="note" >}} - If you are copying the previously listed files, remove the comments (#). - {{< /admonition >}} - -## Use the `grafana_agent` Ansible role - -Next you will create an Ansible playbook that calls the `grafana_agent` role from the `grafana.grafana` Ansible collection. - -To use the `grafana_agent` Ansible role: - -1. Create a file named `deploy-agent.yml` in the same directory as `ansible.cfg` and `inventory` and add the configuration below. - - ```yaml - - name: Install Grafana Agent - hosts: all - become: true - - vars: - grafana_cloud_api_key: - metrics_username: # Example - 825019 - logs_username: # Example - 411478 - prometheus_url: # Example - https://prometheus-us-central1.grafana.net/api/prom/push - loki_url: # Example - https://logs-prod-017.grafana.net/loki/api/v1/push - tasks: - - name: Install Grafana Agent - ansible.builtin.include_role: - name: grafana.grafana.grafana_agent - vars: - grafana_agent_metrics_config: - configs: - - name: integrations - remote_write: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ metrics_username }}' - url: '{{ prometheus_url }}' - - global: - scrape_interval: 60s - wal_directory: /tmp/grafana-agent-wal - grafana_agent_logs_config: - configs: - - name: default - clients: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ logs_username }}' - url: '{{ loki_url }}' - positions: - filename: /tmp/positions.yaml - target_config: - sync_period: 10s - scrape_configs: - - job_name: varlogs - static_configs: - - targets: [localhost] - labels: - instance: ${HOSTNAME:-default} - job: varlogs - __path__: /var/log/*log - grafana_agent_integrations_config: - node_exporter: - enabled: true - instance: ${HOSTNAME:-default} - prometheus_remote_write: - - basic_auth: - password: '{{ grafana_cloud_api_key }}' - username: '{{ metrics_username }}' - url: '{{ prometheus_url }}' - grafana_agent_env_vars: - HOSTNAME: '%H' - ``` - - The playbook calls the `grafana_agent` role from the `grafana.grafana` Ansible collection. - - The Agent configuration in this playbook send metrics and logs from the Linux hosts to Grafana Cloud along with the hostname of each instance - - Refer to the [Grafana Ansible documentation](https://github.com/grafana/grafana-ansible-collection/tree/main/roles/grafana_agent#role-variables) to understand the other variables you can pass to the `grafana_agent` role. - - When deploying the Agent across multiple instances for monitoring them, It is essential that the Agent is able to auto-detect the hostname for ease in monitoring. - Notice that the label `instance` has been set to the value `${HOSTNAME:-default}`, which is substituted by the value of the HOSTNAME environment variable in the Linux host. - - To read more about the variable substitution, refer to the Grafana Agent [node_exporter_config](/docs/grafana-cloud/send-data/agent/static/configuration/integrations/node-exporter-config/) documentation. - -1. To run the playbook, run this command: - - ``` - ansible-playbook deploy-agent.yml - ``` - -{{< admonition type="note" >}} -You can place the `deploy-agent.yml`, `ansible.cfg` and `inventory` files in different directories based on your needs. -{{< /admonition >}} - -## Check that logs and metrics are being ingested into Grafana Cloud - -Logs and metrics will soon be available in Grafana Cloud. -To test this, use the Explore feature. -Click the **Explore** icon (compass icon) in the vertical navigation bar. - -### Check logs - -To check logs: - -1. Use the drop-down menu at the top of the page to select your Loki logs data source. - -1. In the log browser, run the query `{instance="centos-01"}` where `centos-01` is the hostname of one of the Linux hosts. - - If you see log lines (shown in the example below), logs are being received. - - {{< figure alt="Grafana Explore showing a graph and log output from the preceding query" src="/static/assets/img/blog/ansible-to-manage-agent1.png" >}} - - If no log lines appear, logs aren't being collected. - -### Check metrics - -To check metrics: - -1. Use the drop-down menu at the top of the page to select your Prometheus data source. - -1. Run the query `{instance="centos-01"}` where `centos-01` is the hostname of one of the Linux hosts. - - If you see a metrics graph and table (shown in the example below), metrics are being received. - - {{< figure alt="Grafana Explore showing a graph and metrics table output from the preceding query" src="/static/assets/img/blog/ansible-to-manage-agent2.png" >}} - - If no metrics appear, metrics aren't being collected. - -### View dashboards - -Now that you have logs and metrics in Grafana, you can use dashboards to view them. -Here's an example of one of the prebuilt dashboards included with the Linux integration in Grafana Cloud: - -{{< figure alt="The Grafana Node Exporter integration dashboard showing panels of visualizations" src="/static/assets/img/blog/ansible-to-manage-agent3.png" >}} - -Using the **Instance** drop-down in the dashboard, you can select from the hostnames where you deployed Grafana Agent and start monitoring them. - -## Summary - -The `grafana_agent` Ansible role makes it easy to deploy and manage Grafana Agent across multiple machines. -This example showed Grafana Agent deployments across eight Linux hosts, but it's possible to monitor more hosts using the`grafana_agent` role. -To add monitor more Linux hosts, update the `inventory` file and re-run the Ansible playbook. - -To learn more about the Grafana Ansible collection, see its [GitHub repository](https://github.com/grafana/grafana-ansible-collection) or its [documentation](https://docs.ansible.com/ansible/latest/collections/grafana/grafana/index.html). diff --git a/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md b/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md index 3c92270e860..0531a0c803f 100644 --- a/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md +++ b/docs/sources/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd.md @@ -5,295 +5,298 @@ keywords: - Grafana Cloud - Grafana Operator - ArgoCD -title: Manage Dashboards with GitOps Using ArgoCD +title: Manage dashboards with GitOps using ArgoCD +menuTitle: Manage dashboards with ArgoCD weight: 110 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/grafana-operator/manage-dashboards-argocd/ --- -# Managing Grafana Dashboards with GitOps Using ArgoCD +# Manage Grafana dashboards with GitOps using ArgoCD -This guide will walk you through setting up a continuous deployment pipeline using ArgoCD to synchronize your Grafana dashboards with a Git repository. We'll use the Grafana Dashboard Custom Resource provided by the Grafana Operator to manage dashboard configurations declaratively. +This guide shows you how to set up a continuous deployment pipeline using ArgoCD to synchronize your Grafana dashboards with a Git repository. You'll use the Grafana Dashboard Custom Resource provided by the Grafana Operator to manage dashboard configurations declaratively. ## Prerequisites +Before you begin, make sure you have the following: + - An existing Grafana Cloud stack -- A Kubernetes cluster with Grafana Operator installed, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator). -- ArgoCD installed on your Kubernetes cluster. Refer the [Installation Guide](https://argo-cd.readthedocs.io/en/stable/getting_started/). -- Git repository to store your dashboard configurations. +- A Kubernetes cluster with Grafana Operator installed, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator) +- ArgoCD installed on your Kubernetes cluster. Refer to the [ArgoCD Installation Guide](https://argo-cd.readthedocs.io/en/stable/getting_started/) +- A Git repository to store your dashboard configurations -## Set Up Your Git Repository +## Set up your Git repository -Within the repository, create a directory structure to organize your grafana and dashboard configurations. For this tutorial, lets create a folder named `grafana`. +Create a directory structure in your repository to organize your Grafana and dashboard configurations. For this tutorial, create a folder named `grafana`. -## Grafana Operator Setup +## Set up the Grafana Operator -The Grafana Operator allows us to authenticate with the Grafana instance using the Grafana Custom Resource (CR). +The Grafana Operator allows you to authenticate with the Grafana instance using the Grafana Custom Resource (CR). -1. **Create the Grafana API Token Secret:** +### Create the Grafana API Token Secret -Store the Grafana API Token in a secret with the following content in a file named `grafana-token.yml` in the `grafana` folder in your Git repo: +Store the Grafana API Token in a secret. Create a file named `grafana-token.yml` in the `grafana` folder in your Git repository: ```yaml apiVersion: v1 kind: Secret metadata: name: grafana-cloud-credentials - namespace: + namespace: '' stringData: - GRAFANA_CLOUD_INSTANCE_TOKEN: + GRAFANA_CLOUD_INSTANCE_TOKEN: '' type: Opaque ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. To create an API key, refer [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/). -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: API key from your Grafana instance. To create an API key, refer to [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/) +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -2. **Configure the Grafana Custom Resource:** +### Configure the Grafana Custom Resource -Set up connection to your Grafana Cloud instance by creating a file named `grafana-cloud.yml` in the `grafana` folder in your Git repo with the following contents: +Set up the connection to your Grafana Cloud instance. Create a file named `grafana-cloud.yml` in the `grafana` folder in your Git repository: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: Grafana metadata: - name: - namespace: + name: '' + namespace: '' labels: - dashboards: + dashboards: '' spec: external: - url: https://.grafana.net/ + url: https://.grafana.net/ apiKey: name: grafana-cloud-credentials key: GRAFANA_CLOUD_INSTANCE_TOKEN ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of your Grafana Cloud Stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -## Add Dashboards to a Git repository +## Add dashboards to your Git repository -In your `grafana` directory, Create a sub-folder called `dashboards`. For this tutorial, we will create 3 seperate dashboards. +In your `grafana` directory, create a sub-folder called `dashboards`. -1. Under `dashboards` folder, Create a file named `simple-dashboard.yaml` with the following content for the first dashboard: +This guide shows you how to creates three separate dashboards. For all dashboard configurations, replace the placeholders with your values: - ```yaml - apiVersion: grafana.integreatly.org/v1beta1 - kind: GrafanaDashboard - metadata: - name: grafanadashboard-sample - namespace: - spec: - resyncPeriod: 30s - instanceSelector: - matchLabels: - dashboards: - json: > - { - "id": null, - "title": "Simple Dashboard", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "graphTooltip": 1, - "panels": [], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "time_options": [], - "refresh_intervals": [] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "refresh": "5s", - "schemaVersion": 17, - "version": 0, - "links": [] - } - ``` +- _``_: Name of your Grafana Cloud Stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster - Replace the following field values: - - `` with the name of your Grafana Cloud Stack. - - `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +### Create a simple dashboard -1. Under `dashboards` folder, Create a file named `dashboard-from-cm.yaml` with the following content for the second dashboard: +Under the `dashboards` folder, create a file named `simple-dashboard.yaml`: - ```yaml - apiVersion: v1 - kind: ConfigMap - metadata: - name: dashboard-definition - namespace: - spec: - resyncPeriod: 30s - instanceSelector: - matchLabels: - dashboards: - json: > - { - "id": null, - "title": "Simple Dashboard from ConfigMap", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "graphTooltip": 1, - "panels": [], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "time_options": [], - "refresh_intervals": [] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "refresh": "5s", - "schemaVersion": 17, - "version": 0, - "links": [] - } - --- - apiVersion: grafana.integreatly.org/v1beta1 - kind: GrafanaDashboard - metadata: - name: grafanadashboard-from-configmap - namespace: - spec: - instanceSelector: - matchLabels: - dashboards: - configMapRef: - name: dashboard-definition - key: json - ``` +```yaml +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: grafanadashboard-sample + namespace: '' +spec: + resyncPeriod: 30s + instanceSelector: + matchLabels: + dashboards: '' + json: > + { + "id": null, + "title": "Simple Dashboard", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "graphTooltip": 1, + "panels": [], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "time_options": [], + "refresh_intervals": [] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "refresh": "5s", + "schemaVersion": 17, + "version": 0, + "links": [] + } +``` - Replace the following field values: - - `` with the name of your Grafana Cloud Stack. - - `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +### Create a dashboard from ConfigMap -1. Under `dashboards` folder, Create a file named `dashboard-from-id.yaml` with the following content for the third dashboard: +Under the `dashboards` folder, create a file named `dashboard-from-cm.yaml`: - ```yaml - apiVersion: grafana.integreatly.org/v1beta1 - kind: GrafanaDashboard - metadata: - name: node-exporter-latest - namespace: - spec: - instanceSelector: - matchLabels: - dashboards: - grafanaCom: - id: 1860 - ``` +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: dashboard-definition + namespace: +data: + json: > + { + "id": null, + "title": "Simple Dashboard from ConfigMap", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "graphTooltip": 1, + "panels": [], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "time_options": [], + "refresh_intervals": [] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "refresh": "5s", + "schemaVersion": 17, + "version": 0, + "links": [] + } +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: grafanadashboard-from-configmap + namespace: '' +spec: + instanceSelector: + matchLabels: + dashboards: '' + configMapRef: + name: dashboard-definition + key: json +``` - Replace the following field values: - - `` with the name of your Grafana Cloud Stack. - - `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +### Create a dashboard from Grafana.com -## Configure Argo CD to Sync the Git Repository +Under the `dashboards` folder, create a file named `dashboard-from-id.yaml`: -Once all changes are committed to Git, Log in to the Argo CD user interface or use the CLI. +```yaml +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: node-exporter-latest + namespace: '' +spec: + instanceSelector: + matchLabels: + dashboards: '' + grafanaCom: + id: 1860 +``` -2. Create an Argo CD application to manage the synchronization: +## Configure ArgoCD to sync the Git repository - **Using UI**: - - Navigate to 'New App' and fill out the form with your Git repository details and the path to your `grafana` folder. - - Make sure to tick mark directory Recurse. - - Set the sync policy to `Automatic`. +After you commit all changes to Git, log in to the ArgoCD user interface or use the CLI. - **Using CLI**: - - Prepare an application manifest named `argo-application.yaml` with the configuration pointing to your Git repository: +### Create an ArgoCD application - ```yaml - apiVersion: argoproj.io/v1alpha1 - kind: Application - metadata: - name: Grafana - namespace: - spec: - destination: - name: '' - namespace: '' - server: 'https://kubernetes.default.svc' - source: - path: - repoURL: '' - targetRevision: HEAD - directory: - recurse: true - sources: [] - project: default - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - retry: - limit: 2 - backoff: - duration: 5s - maxDuration: 3m0s - factor: 2 - ``` +**Using the UI:** - Replace the following field values: - - `` with the URL of your GIT Repository. - - `` with the path to the `grafana` folder. - - `` with the namespace where ArgoCD is deployed in Kubernetes Cluster. +1. Navigate to **New App** and complete the form with your Git repository details and the path to your `grafana` folder +2. Enable **Directory Recurse** +3. Set the sync policy to **Automatic** - - Create the application in Argo CD: +**Using the CLI:** - ```shell - kubectl apply -f argo-application.yaml - ``` +Prepare an application manifest named `argo-application.yaml`: -## Verify Sync Status in Argo CD +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: Grafana + namespace: '' +spec: + destination: + name: '' + namespace: '' + server: 'https://kubernetes.default.svc' + source: + path: '' + repoURL: '' + targetRevision: HEAD + directory: + recurse: true + sources: [] + project: default + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + retry: + limit: 2 + backoff: + duration: 5s + maxDuration: 3m0s + factor: 2 +``` -1. Monitor the newly created Argo CD application, ensuring that it successfully syncs your dashboard configuration. +Replace the placeholders with your values: -2. Visit the Argo CD dashboard and check the sync status. If it's successful, your Grafana dashboard should be up to date with the configuration from your Git repository. +- _``_: URL of your Git repository +- _``_: Path to the `grafana` folder in your repository +- _``_: Namespace where ArgoCD is deployed in your Kubernetes cluster -## Updating the Dashboards +Create the application in ArgoCD: + +```sh +kubectl apply -f argo-application.yaml +``` + +## Verify sync status in ArgoCD + +1. Monitor the newly created ArgoCD application to ensure it successfully syncs your dashboard configuration +2. Visit the ArgoCD dashboard and check the sync status. If it's successful, your Grafana dashboards should be up to date with the configuration from your Git repository + +## Update your dashboards To update an existing dashboard: -1. Make changes to the dashboard JSON configuration in your Git repository. -2. Commit and push the changes. -3. Argo CD will detect the update and synchronize the changes to your Cutom Resource. -4. Grafana Operator will then sync changes to the Grafana Instance. +1. Make changes to the dashboard JSON configuration in your Git repository +2. Commit and push the changes +3. ArgoCD detects the update and synchronizes the changes to your Custom Resource +4. Grafana Operator then syncs changes to the Grafana instance -## Validating the Grafana Dashboard Update +### Validate your dashboard updates -Log in to your Grafana dashboard and confirm that the changes have been applied. You should see the dashboard update reflected in the Grafana UI. +Log in to your Grafana dashboard and confirm that the changes are applied. You should see the dashboard updates reflected in the Grafana UI. -## Additional Tips +## Next steps -- You can also install the Grafana Operator's Helm Chart using ArgoCD to manage your setup with GitOps. -- You can follow a similar setup for Grafana Dashboards and Folders. +You've successfully set up a GitOps workflow to manage Grafana dashboards using ArgoCD and the Grafana Operator. Your dashboards are now version-controlled and can be consistently deployed across environments. This approach provides a reliable and auditable way to manage observability dashboards and scale your operations. -## Conclusion +To learn more about managing Grafana using Grafana Operator: -You've set up a GitOps workflow to manage Grafana dashboards using Argo CD and the Grafana Operator. Your dashboards are now version-controlled and can be consistently deployed across environments. This approach provides a reliable and auditable way to manage observability dashboards and scale your operations. +- [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/) +- [Grafana dashboard provisioning](/docs/grafana/latest/administration/provisioning/#dashboards) +- [ArgoCD best practices](https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/) -To learn more about managing Grafana using Grafana Operator, see the [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/). +### Additional considerations + +- You can install the Grafana Operator's Helm Chart using ArgoCD to manage your setup with GitOps +- You can follow a similar setup for Grafana Folders and other resources diff --git a/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md b/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md index 9d8617e12c6..e5bbc1467b8 100644 --- a/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md +++ b/docs/sources/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources.md @@ -5,26 +5,27 @@ keywords: - Grafana Cloud - Grafana Operator title: Manage folders, data sources, and dashboards using Grafana Operator +menuTitle: Manage resources with the Grafana Operator weight: 100 canonical: https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/grafana-operator/operator-dashboards-folders-datasources/ --- -# Creating and managing folders, data sources, and dashboards using the Grafana Operator +# Manage folders, data sources, and dashboards using the Grafana Operator -Learn how to manage data sources, folders and dashboard, using Grafana Operator. +This guide shows you how to manage data sources, folders, and dashboards using the Grafana Operator. You'll create these resources declaratively using Kubernetes custom resources. ## Prerequisites -Before you begin, you should have the following available: +Before you begin, make sure you have the following: -- An existing Grafana Cloud stack. -- Grafana Operator Installed in your Cluster, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator). +- An existing Grafana Cloud stack +- Grafana Operator installed in your cluster, as shown in [Grafana Operator Installation](/docs/grafana-cloud/as-code/infrastructure-as-code/grafana-operator/#installing-the-grafana-operator) -## Grafana Operator Setup +## Set up the Grafana Operator -The Grafana Operator allows us to authenticate with the Grafana instance using the Grafana Custom Resource (CR). +The Grafana Operator allows you to authenticate with your Grafana instance using the Grafana Custom Resource (CR). -1. **Create the Grafana API Token Secret:** +### Create the Grafana API Token Secret Store the Grafana API Token in a secret with the following content in a file named `grafana-token.yml`: @@ -33,61 +34,64 @@ apiVersion: v1 kind: Secret metadata: name: grafana-cloud-credentials - namespace: + namespace: '' stringData: - GRAFANA_CLOUD_INSTANCE_TOKEN: + GRAFANA_CLOUD_INSTANCE_TOKEN: '' type: Opaque ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. To create an API key, refer [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/). -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: API key from your Grafana instance. To create an API key, refer to [Grafana API Key Documentation](/docs/grafana/latest/administration/api-keys/) +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -2. **Configure the Grafana Custom Resource:** +### Configure the Grafana Custom Resource -Set up connection to your Grafana Cloud instance by creating a file named `grafana-cloud.yml` with the following contents: +Set up connection to your Grafana Cloud instance. Create a file named `grafana-cloud.yml`: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: Grafana metadata: - name: - namespace: + name: '' + namespace: '' labels: - dashboards: + dashboards: '' spec: external: - url: https://.grafana.net/ + url: https://.grafana.net/ apiKey: name: grafana-cloud-credentials key: GRAFANA_CLOUD_INSTANCE_TOKEN ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with API key from the Grafana instance. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of your Grafana Cloud stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster ## Add a data source -The following steps use the Prometheus data source. The required arguments vary depending on the data source you select. +{{< admonition type="note" >}} -1. **Create the Data Source Configuration:** +This example uses the Prometheus data source. Note that the required arguments vary depending on the data source you select. -Save a new YAML file `datasource.yml` with the following content: +{{< /admonition >}} + +### Create a data source configuration + +Create and save a new YAML file `datasource.yml` with your data source's configuration: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDatasource metadata: - name: - namespace: + name: '' + namespace: '' spec: instanceSelector: matchLabels: - dashboards: + dashboards: '' allowCrossNamespaceImport: true datasource: access: proxy @@ -95,74 +99,76 @@ spec: jsonData: timeInterval: 5s tlsSkipVerify: true - name: + name: '' type: prometheus - url: + url: '' ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with the name of the data source to be added in Grafana. -- `` with URL of your data source. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of the data source to be added in Grafana +- _``_: URL of your data source +- _``_: Name of your Grafana Cloud stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -## Add a dashboard to a folder +### Add a dashboard to a folder -Use the following YAML definition to create a simple dashboard in the Grafana instance under a custom folder. If the folder defined under spec.folder fields doesnt not exist, The operator will create it before placing the dashboard inside the folder. +Use the following YAML definition to create a simple dashboard in the Grafana instance under a custom folder. If the folder defined under the `spec.folder` field doesn't exist, the operator creates it before placing the dashboard inside the folder. -1. **Prepare the Dashboard Configuration File:** - -In `dashboard.yml`, define the dashboard and assign it to a folder: +Prepare the dashboard configuration. In `dashboard.yml`, define the dashboard and assign it to a folder: ```yaml apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDashboard metadata: - name: - namespace: + name: '' + namespace: '' spec: instanceSelector: matchLabels: - dashboards: - folder: "" + dashboards: '' + folder: '' json: > - { - "title": "as-code dashboard", - “uid” : “ascode” - } + { + "title": "as-code dashboard", + "uid" : "ascode" + } ``` -Replace the following field values: +Replace the placeholders with your values: -- `` with the name of the folder in which you want the Dashboard to be created. -- `` with the name of your Grafana Cloud Stack. -- `` with the namespace where the grafana-operator is deployed in Kubernetes Cluster. +- _``_: Name of the folder in which you want the dashboard to be created +- _``_: Name of your Grafana Cloud stack +- _``_: Namespace where the `grafana-operator` is deployed in your Kubernetes cluster -## Apply Kubernetes Manifests +## Apply the Kubernetes manifests In a terminal, run the following commands from the directory where all of the above Kubernetes YAML definitions are located. -1. Create Kubernetes Custom resources for all of the above configurations. +Create Kubernetes Custom resources for all of the configurations: - ```shell - kubectl apply -f grafana-token.yml grafana-cloud.yml datasource.yml dashboard.yml - ``` +```sh +kubectl apply -f grafana-token.yml grafana-cloud.yml datasource.yml dashboard.yml +``` -## Validation +## Validate your configuration -Once you apply the configurations, you should be able to verify the following: +After you apply the configurations, verify that: -- A new data source is visible in Grafana. In the following image a datasource named `InfluxDB` was created. +- A new data source is visible in Grafana. In the following image, a data source named `InfluxDB` was created. ![InfluxDB datasource](/static/img/docs/grafana-cloud/terraform/influxdb_datasource_tf.png) -- A new dashboard and folder in Grafana. In the following image a dashboard named `InfluxDB Cloud Demos` was created inside the `Demos` folder. +- A new dashboard and folder have been created in Grafana. In the following image, a dashboard named `InfluxDB Cloud Demos` was created inside the `Demos` folder. ![InfluxDB dashboard](/static/img/docs/grafana-cloud/grizzly/grizzly-folder-dashboard-datasource.png) -## Conclusion +## Next steps -In this guide, you created a data source, folder, and dashboard using the Grafana Operator. +You've successfully created a data source, folder, and dashboard using the Grafana Operator. Your Grafana resources are now managed declaratively through Kubernetes custom resources. -To learn more about managing Grafana using Grafana Operator, see the [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/). +To learn more about managing Grafana: + +- [Grafana Operator documentation](https://grafana.github.io/grafana-operator/docs/) +- [Grafana dashboard provisioning](/docs/grafana/latest/administration/provisioning/#dashboards) +- [Grafana data source provisioning](/docs/grafana/latest/administration/provisioning/#data-sources) diff --git a/docs/sources/as-code/observability-as-code/schema-v2/_index.md b/docs/sources/as-code/observability-as-code/schema-v2/_index.md index 5eb693f0f1d..9be869a8391 100644 --- a/docs/sources/as-code/observability-as-code/schema-v2/_index.md +++ b/docs/sources/as-code/observability-as-code/schema-v2/_index.md @@ -13,7 +13,7 @@ labels: - enterprise - oss title: JSON schema v2 -weight: 200 +weight: 500 canonical: https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/ aliases: - ../../observability-as-code/schema-v2/ # /docs/grafana/next/observability-as-code/schema-v2/ diff --git a/docs/sources/developer-resources/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json similarity index 100% rename from docs/sources/developer-resources/plugins/plugin.schema.json rename to docs/sources/developers/plugins/plugin.schema.json diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md index 7675d739951..5924e033f90 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/github/index.md @@ -213,7 +213,7 @@ role_attribute_path = [login=='octocat'][0] && 'GrafanaAdmin' || 'Viewer' ## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/) {{< /admonition >}} By using Team Sync, you can map teams from your GitHub organization to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md index 913df8f4792..d3769a72ad8 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/gitlab/index.md @@ -238,7 +238,7 @@ use_refresh_token = true ## Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/) {{< /admonition >}} By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md index df0cc342d38..d122bb7e0f7 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/google/index.md @@ -162,7 +162,7 @@ auto_login = true ### Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/). {{< /admonition >}} With team sync, you can easily add users to teams by utilizing their Google groups. To set up team sync for Google OAuth, refer to the following example. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md index 6a9f6488994..6ce0f7a47b2 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/keycloak/index.md @@ -111,7 +111,7 @@ viewer ## Team sync {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/). {{< /admonition >}} [Team Sync](https://grafana.com/docs/grafana//setup-grafana/configure-access/configure-team-sync/) is a feature that allows you to map groups from your identity provider to Grafana teams. This is useful if you want to give your users access to specific dashboards or folders based on their group membership. diff --git a/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md index 6cb2d7d13c8..7bce3cb59da 100644 --- a/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-access/configure-authentication/okta/index.md @@ -238,7 +238,7 @@ org_mapping = ["Group 1:org_foo:Viewer", "Group 2:org_bar:Editor", "*:3:Editor"] ### Configure team synchronization {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and to customers on select Grafana Cloud plans. For pricing information, visit [pricing](https://grafana.com/pricing/) or contact our sales team. +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/products/cloud/). {{< /admonition >}} By using Team Sync, you can link your Okta groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-access/configure-team-sync.md b/docs/sources/setup-grafana/configure-access/configure-team-sync.md index 1741fd33edc..a4fe4b28c48 100644 --- a/docs/sources/setup-grafana/configure-access/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-access/configure-team-sync.md @@ -18,7 +18,7 @@ weight: 600 Team sync lets you set up synchronization between your auth providers teams and teams in Grafana. This enables LDAP, OAuth, or SAML users who are members of certain teams or groups to automatically be added or removed as members of certain teams in Grafana. {{< admonition type="note" >}} -Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized in the team members list, see `LDAP` label in screenshot. diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index c08056b0f26..c81d24d0758 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -987,15 +987,6 @@ This option is deprecated - assign your viewers as editors, if you are using RBA Viewers can access and use [Explore](../../explore/) and perform temporary edits on panels in dashboards they have access to. They cannot save their changes. Default is `false`. -#### `editors_can_admin` - -{{< admonition type="note" >}} -This option is deprecated - assign your editors as admins, if you are using RBAC assign the team creator role to your users. -{{< /admonition >}} - -Editors can administrate dashboards, folders and teams they create. -Default is `false`. - #### `user_invite_max_lifetime_duration` The duration in time a user invitation remains valid before expiring. diff --git a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md index 601a258764f..7f1cf5e3cb5 100644 --- a/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md +++ b/docs/sources/setup-grafana/configure-grafana/enterprise-configuration/index.md @@ -190,6 +190,10 @@ Name of the TrueType font file with bold style. Name of the TrueType font file with italic style. Default is `DejaVuSansCondensed-Oblique.ttf`. +### font_min_text_size + +The minimum pixel size that Grafana uses when rendering fonts. Default is `4`. + ### max_retries_per_panel Maximum number of times the following reporting rendering requests are retried before returning an error: generating PDFs, generating embedded dashboard images for report emails, and generating attached CSV files. To disable the retry feature, enter `0`. This is available in public preview and requires the `reportingRetries` feature toggle. Default is `3`. 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 317b60971b3..6b76c32fd28 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -31,11 +31,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | | `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes | | `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes | -| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | | `unifiedRequestLog` | Writes error logs to the request logger | Yes | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | Yes | -| `transformationsRedesign` | Enables the transformations redesign | Yes | | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes | | `dashgpt` | Enable AI powered features in dashboards | Yes | | `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | Yes | @@ -44,10 +42,8 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | | `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | | `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | -| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | | `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. | Yes | -| `newPDFRendering` | New implementation for the dashboard-to-PDF rendering | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | | `azureMonitorPrometheusExemplars` | Allows configuration of Azure Monitor as a data source that can provide Prometheus exemplars | Yes | diff --git a/docs/sources/shared/visualizations/panel-zoom.md b/docs/sources/shared/visualizations/panel-zoom.md new file mode 100644 index 00000000000..281f138fc63 --- /dev/null +++ b/docs/sources/shared/visualizations/panel-zoom.md @@ -0,0 +1,19 @@ +--- +title: Panel mouse time zoom +comments: | + This file is used in the following visualizations: candlestick, heatmap, state timeline, status history, time series. +--- + +You can zoom the panel time range in and out, which in turn, changes the dashboard time range. + +**Zoom in** - Click and drag on the panel to zoom in on a particular time range. + +**Zoom out** - Double-click anywhere on the panel to zoom out the time range. + +When you zoom out, the range doubles with each double-click, adding equal time to each side of the range. +For example, if the original time range is from 9:00 to 9:59, the time range changes as follow with each double-click: + +- Next range: 8:30 - 10:29 +- Next range: 7:30 - 11:29 + +For screen recordings showing these interactions, refer to the [Panel overview documentation](https://grafana.com/docs/grafana//visualizations/panels-visualizations/panel-overview/#zoom-panel-time-range). diff --git a/docs/sources/upgrade-guide/upgrade-v12.3/index.md b/docs/sources/upgrade-guide/upgrade-v12.3/index.md new file mode 100644 index 00000000000..8d3dacbf396 --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v12.3/index.md @@ -0,0 +1,20 @@ +--- +description: Guide for upgrading to Grafana v12.3 +keywords: + - grafana + - configuration + - documentation + - upgrade + - '12.3' +title: Upgrade to Grafana v12.3 +menuTitle: Upgrade to v12.3 +weight: 497 +--- + +# Upgrade to Grafana v12.3 + +{{< docs/shared lookup="upgrade/intro_2.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} diff --git a/docs/sources/visualizations/dashboards/build-dashboards/create-template-dashboards/_index.md b/docs/sources/visualizations/dashboards/build-dashboards/create-template-dashboards/_index.md new file mode 100644 index 00000000000..e737b240189 --- /dev/null +++ b/docs/sources/visualizations/dashboards/build-dashboards/create-template-dashboards/_index.md @@ -0,0 +1,50 @@ +--- +keywords: + - grafana + - dashboard + - template +labels: + products: + - cloud + - enterprise + - oss +menuTitle: Create template dashboards +title: Create dashboards from templates +description: Learn how to create dashboards from templates +weight: 3 +--- + +{{< docs/public-preview product="Dashboard templates" >}} + +# Create dashboards from templates + +Grafana provides a variety of pre-built dashboard templates that you can use to quickly set up visualizations for your data. These dashboards use sample data, which you can replace with your own data, making it easier to get started with monitoring and analysis. + +The templates provide standardized dashboard layouts designed to help you answer engineering or business questions consistently. For instance, the DORA template allows all teams within an organization to measure delivery performance using a widely adopted industry framework. + +{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-templates-v12.3.png" max-width="750px" alt="Selection of dashboard templates" >}} + +To create a dashboard from a template, follow these steps: + +1. Click **Dashboards** in the primary menu. +1. Click **New** and select **Dashboard from template** in the drop-down menu. +1. Select a template. + + The dashboard created includes a banner panel indicating the dashboard is using sample data: + + {{< figure src="/media/docs/grafana/dashboards/screenshot-sample-data-dashboard-v12.3.png" max-width="750px" alt="Dashboard with sample data" >}} + +1. Click **Save dashboard** in the top-right corner. +1. Click **Edit**. +1. Update the data source for each panel to add your own data and configure the queries you need. + + {{< admonition type="tip" >}} + In Grafana Cloud, try working with [Grafana Assistant](https://grafana.com/docs/grafana-cloud/machine-learning/assistant/) to update the dashboard with your data sources and to create queries. + {{< /admonition >}} + +1. (Optional) Make any other edits to the dashboard to most effectively display your data. +1. When you've made all of your changes, remove the banner panel. + + {{< figure src="/media/docs/grafana/dashboards/screenshot-remove-banner-v12.3.png" max-width="750px" alt="Removing the sample data banner panel" >}} + +1. Click **Save dashboard**. diff --git a/docs/sources/visualizations/dashboards/create-reports/report-settings/index.md b/docs/sources/visualizations/dashboards/create-reports/report-settings/index.md index 6f131c676a2..0282cc6d84e 100644 --- a/docs/sources/visualizations/dashboards/create-reports/report-settings/index.md +++ b/docs/sources/visualizations/dashboards/create-reports/report-settings/index.md @@ -46,7 +46,7 @@ The options in this section control the branding and theming of the report attac - **Company logo** - Company logo displayed in the report PDF. Configure it by specifying a URL or uploading a file. The maximum file size is 16 MB. - Defaults to the Grafana logo. + If not set, defaults to the Grafana logo. If the specified URL isn't valid, the logo image appears as broken. - **Theme** - Theme of the PDF attached to the report. The selected theme is also applied to the PDFs generated when you click **Preview PDF** during report creation or select the **Export as PDF** option on a dashboard. @@ -66,7 +66,7 @@ The options in this section control the branding and theming of the report attac -- **Company logo** - Company logo displayed in the report email. Configure it by specifying a URL or uploading a file. The maximum file size is 16 MB. Defaults to the Grafana logo. +- **Company logo** - Company logo displayed in the report email. Configure it by specifying a URL or uploading a file. The maximum file size is 16 MB. If not set, defaults to the Grafana logo. If the specified URL isn't valid, the logo image appears as broken. - **Email footer** - Toggle to enable the report email footer. Select **Sent by** or **None**. - **Footer link text** - Text of the link in the report email footer. Defaults to `Grafana`. - **Footer link URL** - Link of the report email footer. diff --git a/docs/sources/visualizations/panels-visualizations/panel-overview/index.md b/docs/sources/visualizations/panels-visualizations/panel-overview/index.md index 299f3963e30..8f6708492a3 100644 --- a/docs/sources/visualizations/panels-visualizations/panel-overview/index.md +++ b/docs/sources/visualizations/panels-visualizations/panel-overview/index.md @@ -175,6 +175,52 @@ By hovering over a panel with the mouse you can use some shortcuts that will tar - `pl`: Hide or show legend - `pr`: Remove Panel +## Zoom panel time range + +You can zoom the panel time range in and out, which in turn, changes the dashboard time range. + +This feature is supported for the following visualizations: + +- Candlestick +- Heatmap +- State timeline +- Status history +- Time series + +### Zoom in + +Click and drag on the panel to zoom in on a particular time range. + +The following screen recordings show this interaction in the time series and x visualizations: + +Time series + +{{< video-embed src="/media/docs/grafana/panels-visualizations/recording-ts-time-zoom-in-mouse.mp4" >}} + +Candlestick + +{{< video-embed src="/media/docs/grafana/panels-visualizations/recording-candle-panel-time-zoom-in-mouse.mp4" >}} + +### Zoom out + +Double-click anywhere on the panel to zoom out the time range. + +The range doubles with each double-click, adding equal time to each side of the range. +For example, if the original time range is from 9:00 to 9:59, the time range changes as follows with each double-click: + +- Next range: 8:30 - 10:29 +- Next range: 7:30 - 11:29 + +The following screen recordings demonstrate the preceding example in the time series and x visualizations: + +Time series + +{{< video-embed src="/media/docs/grafana/panels-visualizations/recording-ts-time-zoom-out-mouse.mp4" >}} + +Heatmap + +{{< video-embed src="/media/docs/grafana/panels-visualizations/recording-heatmap-panel-time-zoom-out-mouse.mp4" >}} + ## Add a panel To add a panel in a new dashboard click **+ Add visualization** in the middle of the dashboard: diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/candlestick/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/candlestick/index.md index bc07d805000..e5760e78199 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/candlestick/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/candlestick/index.md @@ -92,6 +92,10 @@ The data is converted as follows: {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-candles-volume-v11.6.png" max-width="750px" alt="A candlestick visualization showing the price movements of specific asset." >}} +## Zoom panel time range + +{{< docs/shared lookup="visualizations/panel-zoom.md" source="grafana" version="" >}} + ## Configuration options {{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/heatmap/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/heatmap/index.md index 31ffb8295dc..56c91db0e0d 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/heatmap/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/heatmap/index.md @@ -79,6 +79,10 @@ The data is converted as follows: {{< figure src="/static/img/docs/heatmap-panel/heatmap.png" max-width="1025px" alt="A heatmap visualization showing the random walk distribution over time" >}} +## Zoom panel time range + +{{< docs/shared lookup="visualizations/panel-zoom.md" source="grafana" version="" >}} + ## Configuration options {{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/state-timeline/index.md index c5840be810d..ef4066c2b6c 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/state-timeline/index.md @@ -93,6 +93,10 @@ You can also create a state timeline visualization using time series data. To do ![State timeline with time series](/media/docs/grafana/panels-visualizations/screenshot-state-timeline-time-series-v11.4.png) +## Zoom panel time range + +{{< docs/shared lookup="visualizations/panel-zoom.md" source="grafana" version="" >}} + ## Configuration options {{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/status-history/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/status-history/index.md index 4b30df76171..834f50ca733 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/status-history/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/status-history/index.md @@ -85,6 +85,10 @@ The data is converted as follows: {{< figure src="/static/img/docs/status-history-panel/status_history.png" max-width="1025px" alt="A status history panel with two time columns showing the status of two servers" >}} +## Zoom panel time range + +{{< docs/shared lookup="visualizations/panel-zoom.md" source="grafana" version="" >}} + ## Configuration options {{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} diff --git a/docs/sources/visualizations/panels-visualizations/visualizations/time-series/index.md b/docs/sources/visualizations/panels-visualizations/visualizations/time-series/index.md index e7c51fb74cf..a171c88259a 100644 --- a/docs/sources/visualizations/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/visualizations/panels-visualizations/visualizations/time-series/index.md @@ -167,6 +167,10 @@ The following example shows three series: Min, Max, and Value. The Min and Max s {{< docs/shared lookup="visualizations/multiple-y-axes.md" source="grafana" version="" leveloffset="+2" >}} +## Zoom panel time range + +{{< docs/shared lookup="visualizations/panel-zoom.md" source="grafana" version="" >}} + ## Configuration options {{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}} diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index 6779856cdeb..2a320749e66 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -193,6 +193,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 12 +- [What's new in 12.3](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-3) - [What's new in 12.2](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-2) - [What's new in 12.1](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-1) - [What's new in 12.0](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-0) diff --git a/docs/sources/whatsnew/whats-new-in-v12-3.md b/docs/sources/whatsnew/whats-new-in-v12-3.md new file mode 100644 index 00000000000..bcbb2201f65 --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v12-3.md @@ -0,0 +1,59 @@ +--- +description: Feature and improvement highlights for Grafana v12.3 +keywords: + - grafana + - new + - documentation + - '12.3' + - release notes +labels: + products: + - cloud + - enterprise + - oss +title: What's new in Grafana v12.3 +posts: + - title: Interactive learning + items: + - whats-new/2025-10-22-interactive-learning-in-grafana.md + - title: Dashboards and visualizations + items: + - whats-new/2025-08-28-compare-time-series-data-across-time-ranges-in-the-same-panel.md + - whats-new/2025-10-27-switch-variable-type.md + - whats-new/2025-10-28-export-dashboards-as-images.md + - whats-new/2025-10-28-style-table-cells-with-css-properties.md + - whats-new/2025-10-31-save-reuse-and-share-dashboard-annotation-queries.md + - title: Logs + items: + - whats-new/2025-09-09-new-logs-panel.md + - whats-new/2025-10-20-new-logs-context.md + - whats-new/2025-10-22-new-field-selector-component-for-logs.md + - title: Data sources + items: + - whats-new/2025-10-16-unity-catalog-support-in-grafana-databricks-plugin.md + - whats-new/2025-10-01-raw-query-support-in-honeycomb-data-source.md + - whats-new/2025-11-04-cloudwatch-logs-anomaly-detection-and-pattern-analysis.md + - whats-new/2025-11-04-opensearch-ppl-language-support-and-sample-queries.md + - whats-new/2025-10-22-solarwinds-enterprise-data-source-for-grafana/ + - title: Breaking changes + items: + - whats-new/2025-10-03-removal-of-api-key-associated-permissions.md + - whats-new/2025-10-16-change-to-creator-permissions.md + - whats-new/2025-10-16-changes-to-provisioned-permissions.md +whats_new_grafana_version: 12.3 +weight: -52 +--- + +# What’s new in Grafana v12.3 + +Welcome to Grafana 12.3! + +This release reimagines how you explore logs, delivers more ways to connect your data, and enables more intuitive sharing, so you can spend less time navigating tools and more time turning signals into action. It starts with a completely redesigned logs panel that brings faster pattern recognition, clearer context, and a smoother exploration experience. We’ve also expanded what you can do with new data sources and query improvements, from SolarWinds Enterprise to advanced Honeycomb and OpenSearch capabilities. + +Sharing insights is easier too, with streamlined dashboard image exports and consolidated panel time controls that make comparisons and overrides more intuitive. And to help you learn as you go, a new interactive learning experience now brings contextual guidance directly into Grafana, giving you tips, tutorials, and documentation right when you need them. All of this comes together to make open observability easier, more accessible, and more powerful for every team. + +{{< youtube id=VidGeu4PRZw >}} + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v12.3, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v12.3/). + +{{< docs/whats-new >}} diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts index 49d55f8ca04..ef38379f006 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts @@ -22,8 +22,6 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); - await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); - // Should be able to click Variables item in outline to see add variable button await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); await expect( diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts index ce5465c1e33..24c61e5e960 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts @@ -199,7 +199,6 @@ test.describe( .click(); // Open the modal editor in the side pane - await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click(); await openModal(dashboardPage, selectors); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index c59f782b37a..da629ac8141 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -48,7 +48,6 @@ export const flows = { }, async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); - await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx index 25d69140a96..89ed1af12c4 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx @@ -78,6 +78,13 @@ export const plugin = new AppPlugin<{}>() return undefined; } }, + }) + .addLink({ + targets: 'grafana/dynamic/nav-landing-page/nav-id-cfg/cards/v1', + title: 'Extensions Test App', + description: 'This card tests the admin page card extension point', + path: `/a/${pluginJson.id}/`, + category: 'error', }); function isSupported(context?: PluginExtensionPanelContext): boolean { diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json index 0b9f45a25a3..f89721aaac9 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json @@ -19,7 +19,7 @@ "@types/react-dom": "18.3.5", "@types/semver": "7.5.8", "@types/uuid": "9.0.8", - "glob": "10.4.1", + "glob": "10.5.0", "ts-node": "10.9.2", "typescript": "5.5.4", "webpack": "5.95.0", diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json index 57fa87dcde5..3f5adfa215a 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json @@ -55,6 +55,11 @@ "targets": ["grafana/dashboard/panel/menu"], "title": "Open from time series or pie charts (onClick)", "description": "This link will only be visible on time series and pie charts" + }, + { + "targets": ["grafana/dynamic/nav-landing-page/nav-id-cfg/cards/v1"], + "title": "Extensions Test App", + "description": "This card tests the admin page card extension point" } ], "extensionPoints": [ diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 5db3b71773e..84e5d786552 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -742,11 +742,6 @@ "count": 1 } }, - "packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx": { - "no-restricted-syntax": { - "count": 1 - } - }, "packages/grafana-ui/src/components/PanelChrome/PanelContext.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 @@ -1149,11 +1144,6 @@ "count": 1 } }, - "public/app/core/components/Login/LoginCtrl.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/components/Login/LoginForm.tsx": { "no-restricted-syntax": { "count": 2 @@ -1172,9 +1162,6 @@ "public/app/core/components/OptionsUI/NumberInput.tsx": { "no-restricted-syntax": { "count": 1 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/core/components/OptionsUI/fieldColor.tsx": { @@ -1192,11 +1179,6 @@ "count": 1 } }, - "public/app/core/components/OptionsUI/strings.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/components/Page/EditableTitle.tsx": { "no-restricted-syntax": { "count": 1 @@ -1217,11 +1199,6 @@ "count": 1 } }, - "public/app/core/components/Select/TeamPicker.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/core/components/SharedPreferences/SharedPreferences.tsx": { "no-restricted-syntax": { "count": 8 @@ -1407,14 +1384,6 @@ "public/app/features/admin/UserOrgs.tsx": { "no-restricted-syntax": { "count": 2 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, - "public/app/features/admin/UserProfile.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/admin/ldap/LdapDrawer.tsx": { @@ -1836,11 +1805,6 @@ "count": 1 } }, - "public/app/features/annotations/components/AnnotationResultMapper.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx": { "react-prefer-function-component/react-prefer-function-component": { "count": 1 @@ -2331,11 +2295,6 @@ "count": 1 } }, - "public/app/features/dashboard/components/PanelEditor/PanelHeaderCorner.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx": { "@grafana/no-aria-label-selectors": { "count": 1 @@ -2393,17 +2352,11 @@ }, "no-restricted-syntax": { "count": 1 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/dashboard/components/ShareModal/ShareLink.tsx": { "no-restricted-syntax": { "count": 3 - }, - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 } }, "public/app/features/dashboard/components/ShareModal/ShareModal.tsx": { @@ -2752,11 +2705,6 @@ "count": 1 } }, - "public/app/features/explore/RawPrometheus/RawPrometheusContainer.tsx": { - "react-prefer-function-component/react-prefer-function-component": { - "count": 1 - } - }, "public/app/features/explore/RichHistory/RichHistorySettingsTab.tsx": { "no-restricted-syntax": { "count": 1 diff --git a/go.mod b/go.mod index cd27554f792..23756c59ae0 100644 --- a/go.mod +++ b/go.mod @@ -86,9 +86,9 @@ require ( github.com/googleapis/gax-go/v2 v2.15.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // @grafana/identity-access-team + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // @grafana/grafana-backend-group @@ -103,7 +103,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.4.2 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.282.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.283.0 // @grafana/plugins-platform-backend github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // @grafana/alerting-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs github.com/grafana/nanogit v0.0.0-20251106115617-c622d3e0fc4b // indirect; @grafana/grafana-git-ui-sync-team @@ -157,7 +157,7 @@ require ( github.com/prometheus/alertmanager v0.28.0 // @grafana/alerting-backend github.com/prometheus/client_golang v1.23.2 // @grafana/alerting-backend github.com/prometheus/client_model v0.6.2 // @grafana/grafana-backend-group - github.com/prometheus/common v0.67.2 // @grafana/alerting-backend + github.com/prometheus/common v0.67.3 // @grafana/alerting-backend github.com/prometheus/prometheus v0.303.1 // @grafana/alerting-backend github.com/prometheus/sigv4 v0.1.2 // @grafana/alerting-backend github.com/puzpuzpuz/xsync/v4 v4.2.0 // @grafana/grafana-backend-group @@ -197,6 +197,8 @@ require ( go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage go.uber.org/mock v0.6.0 // @grafana/grafana-operator-experience-squad go.uber.org/zap v1.27.0 // @grafana/identity-access-team + go.yaml.in/yaml/v2 v2.4.3 // @grafana/alerting-backend + go.yaml.in/yaml/v3 v3.0.4 // @grafana/alerting-backend gocloud.dev v0.43.0 // @grafana/grafana-app-platform-squad gocloud.dev/secrets/hashivault v0.43.0 // @grafana/grafana-operator-experience-squad golang.org/x/crypto v0.44.0 // @grafana/grafana-backend-group @@ -214,8 +216,6 @@ require ( google.golang.org/protobuf v1.36.10 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group - gopkg.in/yaml.v2 v2.4.0 // @grafana/alerting-backend - gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend k8s.io/api v0.34.2 // @grafana/grafana-app-platform-squad k8s.io/apimachinery v0.34.2 // @grafana/grafana-app-platform-squad k8s.io/apiserver v0.34.2 // @grafana/grafana-app-platform-squad @@ -238,6 +238,7 @@ require ( github.com/grafana/grafana/apps/alerting/notifications v0.0.0 // @grafana/alerting-backend github.com/grafana/grafana/apps/alerting/rules v0.0.0 // @grafana/alerting-backend github.com/grafana/grafana/apps/annotation v0.0.0 // @grafana/grafana-backend-services-squad + github.com/grafana/grafana/apps/collections v0.0.0 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/apps/correlations v0.0.0 // @grafana/datapro github.com/grafana/grafana/apps/dashboard v0.0.0 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b // @grafana/grafana-app-platform-squad @@ -270,6 +271,7 @@ replace ( github.com/grafana/grafana/apps/alerting/notifications => ./apps/alerting/notifications github.com/grafana/grafana/apps/alerting/rules => ./apps/alerting/rules github.com/grafana/grafana/apps/annotation => ./apps/annotation + github.com/grafana/grafana/apps/collections => ./apps/collections github.com/grafana/grafana/apps/correlations => ./apps/correlations github.com/grafana/grafana/apps/dashboard => ./apps/dashboard github.com/grafana/grafana/apps/folder => ./apps/folder @@ -332,7 +334,7 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/at-wat/mqtt-go v0.19.4 // indirect + github.com/at-wat/mqtt-go v0.19.6 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect @@ -626,8 +628,6 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect @@ -645,6 +645,8 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect gopkg.in/telebot.v3 v3.3.8 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.34.2 // indirect k8s.io/kms v0.34.2 // indirect modernc.org/libc v1.66.10 // indirect diff --git a/go.sum b/go.sum index 42d0ecd1703..61068fdef0b 100644 --- a/go.sum +++ b/go.sum @@ -838,8 +838,8 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/at-wat/mqtt-go v0.19.4 h1:R2cbCU7O5PHQ38unbe1Y51ncG3KsFEJV6QeipDoqdLQ= -github.com/at-wat/mqtt-go v0.19.4/go.mod h1:AsiWc9kqVOhqq7LzUeWT/AkKUBfx3Sw5cEe8lc06fqA= +github.com/at-wat/mqtt-go v0.19.6 h1:+jo59yGHB3oqBU8nSFYbq6KDgN4ngMpqts5Qtyq61pc= +github.com/at-wat/mqtt-go v0.19.6/go.mod h1:Y/YrYwUlLE4N53//699Zir3WJnxSdJojvLEAbZxJjgQ= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.22.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= @@ -1611,12 +1611,12 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae h1:NLPwY3tIP0lg0g9wTRiMcypm6VRXW6W+MOLBsq8JSVA= -github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181 h1:nbxKRtrbuhvOYmI2RhOYauHRJCtpR+vTNIgg1lFUCws= +github.com/grafana/alerting v0.0.0-20251119204204-77fa75125181/go.mod h1:VtPNIFlEOJPPEc13Ax6ZTbNV3M/sAzLID72YjgzOPVA= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= @@ -1647,8 +1647,8 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= -github.com/grafana/grafana-plugin-sdk-go v0.282.0 h1:9JT9E6T0h8HsK9emTZ5s01uCKaf0Ok4PjYbb7M/iOjM= -github.com/grafana/grafana-plugin-sdk-go v0.282.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk= github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -2295,8 +2295,8 @@ github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGy github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= diff --git a/go.work b/go.work index c1baa393967..f0f5d9acc19 100644 --- a/go.work +++ b/go.work @@ -10,6 +10,7 @@ use ( ./apps/alerting/notifications ./apps/alerting/rules ./apps/annotation + ./apps/collections ./apps/correlations ./apps/dashboard ./apps/example diff --git a/go.work.sum b/go.work.sum index 8b3ec04e07b..9dc15683534 100644 --- a/go.work.sum +++ b/go.work.sum @@ -133,6 +133,7 @@ cloud.google.com/go/gkehub v0.15.6/go.mod h1:sRT0cOPAgI1jUJrS3gzwdYCJ1NEzVVwmnMK cloud.google.com/go/gkemulticloud v1.5.3 h1:334aZmOzIt3LVBpguCof8IHaLaftcZlx+L0TGBukYkY= cloud.google.com/go/gkemulticloud v1.5.3/go.mod h1:KPFf+/RcfvmuScqwS9/2MF5exZAmXSuoSLPuaQ98Xlk= cloud.google.com/go/grafeas v0.2.0 h1:CYjC+xzdPvbV65gi6Dr4YowKcmLo045pm18L0DhdELM= +cloud.google.com/go/grafeas v0.3.15 h1:lBjwKmhpiqOAFaE0xdqF8CqO74a99s8tUT5mCkBBxPs= cloud.google.com/go/grafeas v0.3.15/go.mod h1:irwcwIQOBlLBotGdMwme8PipnloOPqILfIvMwlmu8Pk= cloud.google.com/go/gsuiteaddons v1.7.7 h1:sk0SxpCGIA7tIO//XdiiG29f2vrF6Pq/dsxxyBGiRBY= cloud.google.com/go/gsuiteaddons v1.7.7/go.mod h1:zTGmmKG/GEBCONsvMOY2ckDiEsq3FN+lzWGUiXccF9o= @@ -276,6 +277,7 @@ github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fw github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4= github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 h1:Ov8avRZi2vmrE2JcXw+tu5K/yB41r7xK9GZDiBF7NdM= github.com/Azure/go-autorest/autorest/azure/auth v0.5.13/go.mod h1:5BAVfWLWXihP47vYrPuBKKf4cS0bXI+KM9Qx6ETDJYo= github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= @@ -302,6 +304,7 @@ github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW5 github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.8 h1:9aTh5GPncdE8BjUn+xanuF/BT3m2BJiyvS50Mmws/fw= github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.8/go.mod h1:exon/I6I+5u/ab7AHmGh0eCXGoYZO5cjqA3wHJlYFFQ= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= @@ -390,6 +393,7 @@ github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4= github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= +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= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/apache/arrow-go/v18 v18.3.0/go.mod h1:eEM1DnUTHhgGAjf/ChvOAQbUQ+EPohtDrArffvUjPg8= @@ -398,6 +402,7 @@ github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6IC github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= github.com/apache/arrow/go/v11 v11.0.0 h1:hqauxvFQxww+0mEU/2XHG6LT7eZternCZq+A5Yly2uM= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= @@ -507,6 +512,8 @@ github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bsm/sarama-cluster v2.1.13+incompatible h1:bqU3gMJbWZVxLZ9PGWVKP05yOmFXUlfw61RBwuE3PYU= github.com/bsm/sarama-cluster v2.1.13+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM= +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= github.com/bufbuild/protovalidate-go v0.9.1/go.mod h1:5jptBxfvlY51RhX32zR6875JfPBRXUsQjyZjm/NqkLQ= github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -620,6 +627,7 @@ github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 h1:qg9VbHo1TlL0KDM0vYvBG9EY0X0Yku5WYIPoFWt8f6o= github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= @@ -627,6 +635,7 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go. github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= +github.com/dgryski/go-ddmin v0.0.0-20210904190556-96a6d69f1034 h1:BuCyszxPxUjBrYW2HNVrimC0rBUs2U27jCJGVh0IKTM= github.com/dgryski/go-ddmin v0.0.0-20210904190556-96a6d69f1034/go.mod h1:zz4KxBkcXUWKjIcrc+uphJ1gPh/t18ymGm3PmQ+VGTk= github.com/dgryski/go-sip13 v0.0.0-20190329191031-25c5027a8c7b h1:Yqiad0+sloMPdd/0Fg22actpFx0dekpzt1xJmVNVkU0= github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= @@ -741,6 +750,7 @@ github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1: github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= @@ -792,6 +802,7 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E= github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= @@ -815,6 +826,7 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/googleapis/cloud-bigtable-clients-test v0.0.3 h1:afMKTvA/jc6jSTMkeHBZGFDTt8Cc+kb1ATFzqMK85hw= github.com/googleapis/cloud-bigtable-clients-test v0.0.3/go.mod h1:TWtDzrrAI70C3dNLDY+nZN3gxHtFdZIbpL9rCTFyxE0= github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0= @@ -837,6 +849,7 @@ github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+ github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw= +github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38= github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E= github.com/grafana/dskit v0.0.0-20250818234656-8ff9c6532e85/go.mod h1:kImsvJ1xnmeT9Z6StK+RdEKLzlpzBsKwJbEQfmBJdFs= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= @@ -1881,6 +1894,7 @@ golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= @@ -1936,12 +1950,14 @@ golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= @@ -1958,6 +1974,7 @@ golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= @@ -2034,6 +2051,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 7a6e5898490..59976c9bf3c 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -90,6 +90,7 @@ grafana::codegen:run apps/dashboard/pkg grafana::codegen:run apps/provisioning/pkg grafana::codegen:run apps/folder/pkg grafana::codegen:run apps/preferences/pkg +grafana::codegen:run apps/collections/pkg grafana::codegen:run apps/scope/pkg grafana::codegen:run apps/alerting/alertenrichment/pkg diff --git a/packages/grafana-api-clients/package.json b/packages/grafana-api-clients/package.json index 79809210045..37fdf558cf3 100644 --- a/packages/grafana-api-clients/package.json +++ b/packages/grafana-api-clients/package.json @@ -72,6 +72,10 @@ "import": "./src/clients/rtkq/preferences/v1alpha1/index.ts", "require": "./src/clients/rtkq/preferences/v1alpha1/index.ts" }, + "./rtkq/collections/v1alpha1": { + "import": "./src/clients/rtkq/collections/v1alpha1/index.ts", + "require": "./src/clients/rtkq/collections/v1alpha1/index.ts" + }, "./rtkq/provisioning/v0alpha1": { "import": "./src/clients/rtkq/provisioning/v0alpha1/index.ts", "require": "./src/clients/rtkq/provisioning/v0alpha1/index.ts" diff --git a/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/baseAPI.ts b/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/baseAPI.ts new file mode 100644 index 00000000000..9e637edb295 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/baseAPI.ts @@ -0,0 +1,16 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; + +import { getAPIBaseURL } from '../../../../utils/utils'; +import { createBaseQuery } from '../../createBaseQuery'; + +export const API_GROUP = 'collections.grafana.app' as const; +export const API_VERSION = 'v1alpha1' as const; +export const BASE_URL = getAPIBaseURL(API_GROUP, API_VERSION); + +export const api = createApi({ + reducerPath: 'collectionsAPIv1alpha1', + baseQuery: createBaseQuery({ + baseURL: BASE_URL, + }), + endpoints: () => ({}), +}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/endpoints.gen.ts new file mode 100644 index 00000000000..9e2b5c0b534 --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/endpoints.gen.ts @@ -0,0 +1,538 @@ +import { api } from './baseAPI'; +export const addTagTypes = ['API Discovery', 'Stars'] as const; +const injectedRtkApi = api + .enhanceEndpoints({ + addTagTypes, + }) + .injectEndpoints({ + endpoints: (build) => ({ + getApiResources: build.query({ + query: () => ({ url: `/` }), + providesTags: ['API Discovery'], + }), + listStars: build.query({ + query: (queryArg) => ({ + url: `/stars`, + params: { + pretty: queryArg.pretty, + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['Stars'], + }), + createStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars`, + method: 'POST', + body: queryArg.stars, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Stars'], + }), + deletecollectionStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + continue: queryArg['continue'], + dryRun: queryArg.dryRun, + fieldSelector: queryArg.fieldSelector, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + }, + }), + invalidatesTags: ['Stars'], + }), + getStars: build.query({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Stars'], + }), + replaceStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + method: 'PUT', + body: queryArg.stars, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + }, + }), + invalidatesTags: ['Stars'], + }), + deleteStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['Stars'], + }), + updateStars: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}`, + method: 'PATCH', + body: queryArg.patch, + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + fieldManager: queryArg.fieldManager, + fieldValidation: queryArg.fieldValidation, + force: queryArg.force, + }, + }), + invalidatesTags: ['Stars'], + }), + addStar: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}/update/${queryArg.group}/${queryArg.kind}/${queryArg.id}`, + method: 'PUT', + }), + invalidatesTags: ['Stars'], + }), + removeStar: build.mutation({ + query: (queryArg) => ({ + url: `/stars/${queryArg.name}/update/${queryArg.group}/${queryArg.kind}/${queryArg.id}`, + method: 'DELETE', + }), + invalidatesTags: ['Stars'], + }), + }), + overrideExisting: false, + }); +export { injectedRtkApi as generatedAPI }; +export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; +export type GetApiResourcesApiArg = void; +export type ListStarsApiResponse = /** status 200 OK */ StarsList; +export type ListStarsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateStarsApiResponse = /** status 200 OK */ + | Stars + | /** status 201 Created */ Stars + | /** status 202 Accepted */ Stars; +export type CreateStarsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + stars: Stars; +}; +export type DeletecollectionStarsApiResponse = /** status 200 OK */ Status; +export type DeletecollectionStarsApiArg = { + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; +}; +export type GetStarsApiResponse = /** status 200 OK */ Stars; +export type GetStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type ReplaceStarsApiResponse = /** status 200 OK */ Stars | /** status 201 Created */ Stars; +export type ReplaceStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + stars: Stars; +}; +export type DeleteStarsApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type UpdateStarsApiResponse = /** status 200 OK */ Stars | /** status 201 Created */ Stars; +export type UpdateStarsApiArg = { + /** name of the Stars */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ + fieldManager?: string; + /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ + fieldValidation?: string; + /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ + force?: boolean; + patch: Patch; +}; +export type AddStarApiResponse = /** status 200 OK */ Stars; +export type AddStarApiArg = { + /** name of the Stars */ + name: string; + /** API group for stared item */ + group: string; + /** Kind for stared item */ + kind: string; + /** The k8s name for the selected item */ + id: string; +}; +export type RemoveStarApiResponse = /** status 200 OK */ Stars; +export type RemoveStarApiArg = { + /** name of the Stars */ + name: string; + /** API group for stared item */ + group: string; + /** Kind for stared item */ + kind: string; + /** The k8s name for the selected item */ + id: string; +}; +export type ApiResource = { + /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ + categories?: string[]; + /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ + group?: string; + /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ + kind: string; + /** name is the plural name of the resource. */ + name: string; + /** namespaced indicates if a resource is namespaced or not. */ + namespaced: boolean; + /** shortNames is a list of suggested short names of the resource. */ + shortNames?: string[]; + /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ + singularName: string; + /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ + storageVersionHash?: string; + /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ + verbs: string[]; + /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ + version?: string; +}; +export type ApiResourceList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** groupVersion is the group and version this APIResourceList is for. */ + groupVersion: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** resources contains the name of the resources and if they are namespaced. */ + resources: ApiResource[]; +}; +export type Time = string; +export type FieldsV1 = object; +export type ManagedFieldsEntry = { + /** APIVersion defines the version of this resource that this field set applies to. The format is "group/version" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted. */ + apiVersion?: string; + /** FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: "FieldsV1" */ + fieldsType?: string; + /** FieldsV1 holds the first JSON version format as described in the "FieldsV1" type. */ + fieldsV1?: FieldsV1; + /** Manager is an identifier of the workflow managing these fields. */ + manager?: string; + /** Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'. */ + operation?: string; + /** Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource. */ + subresource?: string; + /** Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over. */ + time?: Time; +}; +export type OwnerReference = { + /** API version of the referent. */ + apiVersion: string; + /** If true, AND if the owner has the "foregroundDeletion" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs "delete" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned. */ + blockOwnerDeletion?: boolean; + /** If true, this reference points to the managing controller. */ + controller?: boolean; + /** Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind: string; + /** Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name: string; + /** UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid: string; +}; +export type ObjectMeta = { + /** Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations */ + annotations?: { + [key: string]: string; + }; + /** CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC. + + Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + creationTimestamp?: Time; + /** Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. */ + deletionGracePeriodSeconds?: number; + /** DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. + + Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata */ + deletionTimestamp?: Time; + /** Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list. */ + finalizers?: string[]; + /** GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. + + If this field is specified and the generated name exists, the server will return a 409. + + Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency */ + generateName?: string; + /** A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. */ + generation?: number; + /** Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels */ + labels?: { + [key: string]: string; + }; + /** ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like "ci-cd". The set of fields is always in the version that the workflow used when modifying the object. */ + managedFields?: ManagedFieldsEntry[]; + /** Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names */ + name?: string; + /** Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty. + + Must be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces */ + namespace?: string; + /** List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller. */ + ownerReferences?: OwnerReference[]; + /** An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. + + Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ + resourceVersion?: string; + /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ + selfLink?: string; + /** UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. + + Populated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type StarsResource = { + group: string; + kind: string; + /** The set of resources */ + names: string[]; +}; +export type StarsSpec = { + resource: StarsResource[]; +}; +export type Stars = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the Stars */ + spec: StarsSpec; +}; +export type ListMeta = { + /** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */ + continue?: string; + /** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */ + remainingItemCount?: number; + /** String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */ + resourceVersion?: string; + /** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */ + selfLink?: string; +}; +export type StarsList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: Stars[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; +export type StatusCause = { + /** The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. + + Examples: + "name" - the field "name" on the current resource + "items[0].name" - the field "name" on the first array entry in "items" */ + field?: string; + /** A human-readable description of the cause of the error. This field may be presented as-is to a reader. */ + message?: string; + /** A machine-readable description of the cause of the error. If this value is empty there is no information available. */ + reason?: string; +}; +export type StatusDetails = { + /** The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. */ + causes?: StatusCause[]; + /** The group attribute of the resource associated with the status StatusReason. */ + group?: string; + /** The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). */ + name?: string; + /** If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. */ + retryAfterSeconds?: number; + /** UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids */ + uid?: string; +}; +export type Status = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Suggested HTTP return code for this status, 0 if not set. */ + code?: number; + /** Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. */ + details?: StatusDetails; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** A human-readable description of the status of this operation. */ + message?: string; + /** Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + metadata?: ListMeta; + /** A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. */ + reason?: string; + /** Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status */ + status?: string; +}; +export type Patch = object; +export const { + useGetApiResourcesQuery, + useLazyGetApiResourcesQuery, + useListStarsQuery, + useLazyListStarsQuery, + useCreateStarsMutation, + useDeletecollectionStarsMutation, + useGetStarsQuery, + useLazyGetStarsQuery, + useReplaceStarsMutation, + useDeleteStarsMutation, + useUpdateStarsMutation, + useAddStarMutation, + useRemoveStarMutation, +} = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/index.ts b/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/index.ts new file mode 100644 index 00000000000..d80fd6d553a --- /dev/null +++ b/packages/grafana-api-clients/src/clients/rtkq/collections/v1alpha1/index.ts @@ -0,0 +1,5 @@ +export { BASE_URL, API_GROUP, API_VERSION } from './baseAPI'; +import { generatedAPI as rawAPI } from './endpoints.gen'; + +export * from './endpoints.gen'; +export const generatedAPI = rawAPI.enhanceEndpoints({}); diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index 26f8053d29c..264ac389e42 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -245,6 +245,7 @@ const injectedRtkApi = api facet: queryArg.facet, tags: queryArg.tags, libraryPanel: queryArg.libraryPanel, + permission: queryArg.permission, sort: queryArg.sort, limit: queryArg.limit, explain: queryArg.explain, @@ -611,6 +612,8 @@ export type GetSearchApiArg = { tags?: string[]; /** find dashboards that reference a given libraryPanel */ libraryPanel?: string; + /** permission needed for the resource (View, Edit, Admin) */ + permission?: 'View' | 'Edit' | 'Admin'; /** sortable field */ sort?: string; /** number of results to return */ diff --git a/packages/grafana-api-clients/src/clients/rtkq/index.ts b/packages/grafana-api-clients/src/clients/rtkq/index.ts index 4bdede94ed9..68dba9c3e38 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/index.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/index.ts @@ -2,6 +2,7 @@ * Don't manually add to this file! Use the `generate:api-client` command to add new API clients. */ import { generatedAPI as advisorAPIv0alpha1 } from './advisor/v0alpha1'; +import { generatedAPI as collectionsAPIv1alpha1 } from './collections/v1alpha1'; import { generatedAPI as correlationsAPIv0alpha1 } from './correlations/v0alpha1'; import { generatedAPI as dashboardAPIv0alpha1 } from './dashboard/v0alpha1'; import { generatedAPI as folderAPIv1beta1 } from './folder/v1beta1'; @@ -23,8 +24,9 @@ export const allMiddleware = [ iamAPIv0alpha1.middleware, migrateToCloudAPI.middleware, playlistAPIv0alpha1.middleware, + collectionsAPIv1alpha1.middleware, // stars preferencesAPIv1alpha1.middleware, - preferencesUserAPI.middleware, + preferencesUserAPI.middleware, // legacy preferences provisioningAPIv0alpha1.middleware, shortURLAPIv1beta1.middleware, correlationsAPIv0alpha1.middleware, @@ -40,6 +42,7 @@ export const allReducers = { [iamAPIv0alpha1.reducerPath]: iamAPIv0alpha1.reducer, [migrateToCloudAPI.reducerPath]: migrateToCloudAPI.reducer, [playlistAPIv0alpha1.reducerPath]: playlistAPIv0alpha1.reducer, + [collectionsAPIv1alpha1.reducerPath]: collectionsAPIv1alpha1.reducer, [preferencesAPIv1alpha1.reducerPath]: preferencesAPIv1alpha1.reducer, [preferencesUserAPI.reducerPath]: preferencesUserAPI.reducer, [provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer, diff --git a/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts index c90f694defb..adfb1d8297a 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/preferences/v1alpha1/endpoints.gen.ts @@ -1,5 +1,5 @@ import { api } from './baseAPI'; -export const addTagTypes = ['API Discovery', 'Preferences', 'Stars'] as const; +export const addTagTypes = ['API Discovery', 'Preferences'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, @@ -100,129 +100,6 @@ const injectedRtkApi = api }), invalidatesTags: ['Preferences'], }), - listStars: build.query({ - query: (queryArg) => ({ - url: `/stars`, - params: { - pretty: queryArg.pretty, - allowWatchBookmarks: queryArg.allowWatchBookmarks, - continue: queryArg['continue'], - fieldSelector: queryArg.fieldSelector, - labelSelector: queryArg.labelSelector, - limit: queryArg.limit, - resourceVersion: queryArg.resourceVersion, - resourceVersionMatch: queryArg.resourceVersionMatch, - sendInitialEvents: queryArg.sendInitialEvents, - timeoutSeconds: queryArg.timeoutSeconds, - watch: queryArg.watch, - }, - }), - providesTags: ['Stars'], - }), - createStars: build.mutation({ - query: (queryArg) => ({ - url: `/stars`, - method: 'POST', - body: queryArg.stars, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['Stars'], - }), - deletecollectionStars: build.mutation({ - query: (queryArg) => ({ - url: `/stars`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - continue: queryArg['continue'], - dryRun: queryArg.dryRun, - fieldSelector: queryArg.fieldSelector, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - labelSelector: queryArg.labelSelector, - limit: queryArg.limit, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - resourceVersion: queryArg.resourceVersion, - resourceVersionMatch: queryArg.resourceVersionMatch, - sendInitialEvents: queryArg.sendInitialEvents, - timeoutSeconds: queryArg.timeoutSeconds, - }, - }), - invalidatesTags: ['Stars'], - }), - getStars: build.query({ - query: (queryArg) => ({ - url: `/stars/${queryArg.name}`, - params: { - pretty: queryArg.pretty, - }, - }), - providesTags: ['Stars'], - }), - replaceStars: build.mutation({ - query: (queryArg) => ({ - url: `/stars/${queryArg.name}`, - method: 'PUT', - body: queryArg.stars, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['Stars'], - }), - deleteStars: build.mutation({ - query: (queryArg) => ({ - url: `/stars/${queryArg.name}`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - }, - }), - invalidatesTags: ['Stars'], - }), - updateStars: build.mutation({ - query: (queryArg) => ({ - url: `/stars/${queryArg.name}`, - method: 'PATCH', - body: queryArg.patch, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - force: queryArg.force, - }, - }), - invalidatesTags: ['Stars'], - }), - addStar: build.mutation({ - query: (queryArg) => ({ - url: `/stars/${queryArg.name}/update/${queryArg.group}/${queryArg.kind}/${queryArg.id}`, - method: 'PUT', - }), - invalidatesTags: ['Stars'], - }), - removeStar: build.mutation({ - query: (queryArg) => ({ - url: `/stars/${queryArg.name}/update/${queryArg.group}/${queryArg.kind}/${queryArg.id}`, - method: 'DELETE', - }), - invalidatesTags: ['Stars'], - }), }), overrideExisting: false, }); @@ -345,193 +222,6 @@ export type UpdatePreferencesApiArg = { force?: boolean; patch: Patch; }; -export type ListStarsApiResponse = /** status 200 OK */ StarsList; -export type ListStarsApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ - allowWatchBookmarks?: boolean; - /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - - This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ - continue?: string; - /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ - fieldSelector?: string; - /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ - labelSelector?: string; - /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - - The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ - limit?: number; - /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersion?: string; - /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersionMatch?: string; - /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - - When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan - is interpreted as "data at least as new as the provided `resourceVersion`" - and the bookmark event is send when the state is synced - to a `resourceVersion` at least as fresh as the one provided by the ListOptions. - If `resourceVersion` is unset, this is interpreted as "consistent read" and the - bookmark event is send when the state is synced at least to the moment - when request started being processed. - - `resourceVersionMatch` set to any other value or unset - Invalid error is returned. - - Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ - sendInitialEvents?: boolean; - /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ - timeoutSeconds?: number; - /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ - watch?: boolean; -}; -export type CreateStarsApiResponse = /** status 200 OK */ - | Stars - | /** status 201 Created */ Stars - | /** status 202 Accepted */ Stars; -export type CreateStarsApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - stars: Stars; -}; -export type DeletecollectionStarsApiResponse = /** status 200 OK */ Status; -export type DeletecollectionStarsApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - - This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ - continue?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ - fieldSelector?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ - labelSelector?: string; - /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - - The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ - limit?: number; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; - /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersion?: string; - /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersionMatch?: string; - /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - - When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan - is interpreted as "data at least as new as the provided `resourceVersion`" - and the bookmark event is send when the state is synced - to a `resourceVersion` at least as fresh as the one provided by the ListOptions. - If `resourceVersion` is unset, this is interpreted as "consistent read" and the - bookmark event is send when the state is synced at least to the moment - when request started being processed. - - `resourceVersionMatch` set to any other value or unset - Invalid error is returned. - - Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ - sendInitialEvents?: boolean; - /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ - timeoutSeconds?: number; -}; -export type GetStarsApiResponse = /** status 200 OK */ Stars; -export type GetStarsApiArg = { - /** name of the Stars */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; -}; -export type ReplaceStarsApiResponse = /** status 200 OK */ Stars | /** status 201 Created */ Stars; -export type ReplaceStarsApiArg = { - /** name of the Stars */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - stars: Stars; -}; -export type DeleteStarsApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; -export type DeleteStarsApiArg = { - /** name of the Stars */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; -}; -export type UpdateStarsApiResponse = /** status 200 OK */ Stars | /** status 201 Created */ Stars; -export type UpdateStarsApiArg = { - /** name of the Stars */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch). */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - /** Force is going to "force" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests. */ - force?: boolean; - patch: Patch; -}; -export type AddStarApiResponse = /** status 200 OK */ Stars; -export type AddStarApiArg = { - /** name of the Stars */ - name: string; - /** API group for stared item */ - group: string; - /** Kind for stared item */ - kind: string; - /** The k8s name for the selected item */ - id: string; -}; -export type RemoveStarApiResponse = /** status 200 OK */ Stars; -export type RemoveStarApiArg = { - /** name of the Stars */ - name: string; - /** API group for stared item */ - group: string; - /** Kind for stared item */ - kind: string; - /** The k8s name for the selected item */ - id: string; -}; export type ApiResource = { /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ categories?: string[]; @@ -750,32 +440,6 @@ export type Status = { status?: string; }; export type Patch = object; -export type StarsResource = { - group: string; - kind: string; - /** The set of resources */ - names: string[]; -}; -export type StarsSpec = { - resource: StarsResource[]; -}; -export type Stars = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata: ObjectMeta; - /** Spec is the spec of the Stars */ - spec: StarsSpec; -}; -export type StarsList = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - items: Stars[]; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata: ListMeta; -}; export const { useGetApiResourcesQuery, useLazyGetApiResourcesQuery, @@ -789,15 +453,4 @@ export const { useReplacePreferencesMutation, useDeletePreferencesMutation, useUpdatePreferencesMutation, - useListStarsQuery, - useLazyListStarsQuery, - useCreateStarsMutation, - useDeletecollectionStarsMutation, - useGetStarsQuery, - useLazyGetStarsQuery, - useReplaceStarsMutation, - useDeleteStarsMutation, - useUpdateStarsMutation, - useAddStarMutation, - useRemoveStarMutation, } = injectedRtkApi; diff --git a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts index 8b211fbca8c..776ad4131ae 100644 --- a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts +++ b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts @@ -104,6 +104,7 @@ const config: ConfigFile = { ...createAPIConfig('folder', 'v1beta1'), ...createAPIConfig('iam', 'v0alpha1'), ...createAPIConfig('playlist', 'v0alpha1'), + ...createAPIConfig('collections', 'v1alpha1'), ...createAPIConfig('preferences', 'v1alpha1'), ...createAPIConfig('provisioning', 'v0alpha1'), ...createAPIConfig('shorturl', 'v1beta1'), diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts index a8259b226d9..9b0342fef40 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts @@ -1,19 +1,10 @@ import { toDataFrame } from '../../dataframe/processDataFrame'; -import { BootData } from '../../types/config'; import { DataFrame, FieldType } from '../../types/dataFrame'; import { getFieldMatcher } from '../matchers'; import { FieldMatcherID } from './ids'; import { ByNamesMatcherMode } from './nameMatcher'; -window.grafanaBootData = { - settings: { - featureToggles: { - dataplaneFrontendFallback: true, - }, - }, -} as BootData; - describe('Field Name by Regexp Matcher', () => { it('Match all with wildcard regex', () => { const seriesWithNames = toDataFrame({ diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts index 5a344ed4abd..142c8090e7a 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts @@ -107,22 +107,17 @@ const multipleFieldNamesMatcher: FieldMatcherInfo = { export function fieldNameFallback(fields: Set) { let fallback: FieldMatcher | undefined = undefined; - // grafana-data does not have access to runtime so we are accessing the window object - // to get access to the feature toggle - const useMatcherFallback = window.grafanaBootData?.settings?.featureToggles?.dataplaneFrontendFallback; - if (useMatcherFallback) { - if (fields.has(TIME_SERIES_VALUE_FIELD_NAME)) { - fallback = (field: Field, frame: DataFrame) => { - return ( - Boolean(field.labels) && // Value was reasonable when the name was set in labels or on the frame - field.labels?.__name__ === field.name - ); - }; - } else if (fields.has('Time') || fields.has('time')) { - fallback = (field: Field, frame: DataFrame) => { - return frame.meta?.typeVersion == null && field.type === FieldType.time; - }; - } + if (fields.has(TIME_SERIES_VALUE_FIELD_NAME)) { + fallback = (field: Field, frame: DataFrame) => { + return ( + Boolean(field.labels) && // Value was reasonable when the name was set in labels or on the frame + field.labels?.__name__ === field.name + ); + }; + } else if (fields.has('Time') || fields.has('time')) { + fallback = (field: Field, frame: DataFrame) => { + return frame.meta?.typeVersion == null && field.type === FieldType.time; + }; } return fallback; diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts index 4e388bf3257..8281ecc46aa 100644 --- a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts @@ -1,6 +1,5 @@ import { map } from 'rxjs/operators'; -import { getFieldDisplayName } from '../../field/fieldState'; import { DataFrame, Field, FieldType } from '../../types/dataFrame'; import { SpecialValue, @@ -26,10 +25,6 @@ const DEFAULT_ROW_FIELD = 'Time'; const DEFAULT_VALUE_FIELD = 'Value'; const DEFAULT_EMPTY_VALUE = SpecialValue.Empty; -// grafana-data does not have access to runtime so we are accessing the window object -// to get access to the feature toggle -const supportDataplaneFallback = window.grafanaBootData?.settings?.featureToggles?.dataplaneFrontendFallback; - export const groupingToMatrixTransformer: DataTransformerInfo = { id: DataTransformerID.groupingToMatrix, name: 'Grouping to Matrix', @@ -125,7 +120,7 @@ export const groupingToMatrixTransformer: DataTransformerInfo `Data source list item ${dataSourceName}`, }, + dataSourceAddButton: { + '12.4.0': 'data-testid data-source-add-button', + }, }, EditDataSource: { url: { diff --git a/packages/grafana-plugin-configs/webpack.config.ts b/packages/grafana-plugin-configs/webpack.config.ts index 7cc76782449..6f2846b5a97 100644 --- a/packages/grafana-plugin-configs/webpack.config.ts +++ b/packages/grafana-plugin-configs/webpack.config.ts @@ -4,7 +4,7 @@ import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import path from 'path'; import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; -import webpack, { type Configuration } from 'webpack'; +import webpack, { type Configuration, type Compiler } from 'webpack'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import VirtualModulesPlugin from 'webpack-virtual-modules'; @@ -31,6 +31,36 @@ function skipFiles(f: string): boolean { return true; } +class BuildModeWebpackPlugin { + apply(compiler: Compiler) { + compiler.hooks.compilation.tap('BuildModeWebpackPlugin', (compilation) => { + compilation.hooks.processAssets.tap( + { + name: 'BuildModeWebpackPlugin', + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + async () => { + const assets = compilation.getAssets(); + for (const asset of assets) { + if (asset.name.endsWith('plugin.json')) { + const pluginJsonString = asset.source.source().toString(); + const pluginJsonWithBuildMode = JSON.stringify( + { + ...JSON.parse(pluginJsonString), + buildMode: compilation.options.mode, + }, + null, + 4 + ); + compilation.updateAsset(asset.name, new webpack.sources.RawSource(pluginJsonWithBuildMode)); + } + } + } + ); + }); + } +} + export type Env = { [key: string]: true | string | Env; }; @@ -253,6 +283,8 @@ const config = async (env: Env): Promise => { ], }, ]), + // Add buildMode to plugin.json + new BuildModeWebpackPlugin(), ...(env.development ? [ new ForkTsCheckerWebpackPlugin({ diff --git a/packages/grafana-prometheus/src/components/metrics-browser/ValueSelector.tsx b/packages/grafana-prometheus/src/components/metrics-browser/ValueSelector.tsx index d2918498bec..e0a26f2e8e4 100644 --- a/packages/grafana-prometheus/src/components/metrics-browser/ValueSelector.tsx +++ b/packages/grafana-prometheus/src/components/metrics-browser/ValueSelector.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { FixedSizeList } from 'react-window'; import { selectors } from '@grafana/e2e-selectors'; @@ -17,6 +17,16 @@ export function ValueSelector() { const [valueSearchTerm, setValueSearchTerm] = useState(''); const { labelValues, selectedLabelValues, isLoadingLabelValues, onLabelValueClick, onLabelKeyClick } = useMetricsBrowser(); + const [filteredLabelValues, setFilteredLabelValues] = useState>({ ...labelValues }); + + useEffect(() => { + const filtered: Record = {}; + for (const labelKey in labelValues) { + const values = labelValues[labelKey]; + filtered[labelKey] = values.filter((value) => value.includes(valueSearchTerm)); + } + setFilteredLabelValues(filtered); + }, [labelValues, valueSearchTerm]); return (
@@ -47,7 +57,7 @@ export function ValueSelector() {
) : (
- {Object.entries(labelValues).map(([lk, lv]) => { + {Object.entries(filteredLabelValues).map(([lk, lv]) => { if (!lk || !lv) { console.error('label values are empty:', { lk, lv }); return null; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx index 8250bd91a68..dc0a5799259 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx @@ -118,16 +118,18 @@ export const PromQueryEditorSelector = memo((props) => { onAddQuery={onAddQuery} /> - + {!query.expr && ( + + )}
- http.get('/apis/preferences.grafana.app/v1alpha1/namespaces/:namespace/stars', () => { + http.get('/apis/collections.grafana.app/v1alpha1/namespaces/:namespace/stars', () => { const mockStarsResponse = { kind: 'StarsList', - apiVersion: 'preferences.grafana.app/v1alpha1', + apiVersion: 'collections.grafana.app/v1alpha1', metadata: { resourceVersion: '1758126936000', }, @@ -35,7 +35,7 @@ const getStarsHandler = () => }); const UPDATE_STARS_URL = - '/apis/preferences.grafana.app/v1alpha1/namespaces/:namespace/stars/:name/update/:group/:kind/:id'; + '/apis/collections.grafana.app/v1alpha1/namespaces/:namespace/stars/:name/update/:group/:kind/:id'; type UpdateOrDeleteStarsParams = { namespace: string; diff --git a/packages/grafana-test-utils/src/server/index.ts b/packages/grafana-test-utils/src/server/index.ts index 71636cda7ac..8a4c417c77c 100644 --- a/packages/grafana-test-utils/src/server/index.ts +++ b/packages/grafana-test-utils/src/server/index.ts @@ -11,7 +11,7 @@ const server = setupServer(...allHandlers); */ export function setupMockServer( /** - * Additional handlers to add to server initialisation. Handlers will be `.use`d in a `beforeEach` hook + * Additional handlers to add to server initialization. Handlers will be `.use`d in a `beforeEach` hook */ additionalHandlers?: HttpHandler[] ) { diff --git a/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx b/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx index d7a85381e9e..7e993ac8a25 100644 --- a/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx +++ b/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx @@ -1,9 +1,7 @@ import { action } from '@storybook/addon-actions'; import { Meta } from '@storybook/react'; -import { StoryExample } from '../../utils/storybook/StoryExample'; import { IconButton } from '../IconButton/IconButton'; -import { Stack } from '../Layout/Stack/Stack'; import { ToolbarButton } from '../ToolbarButton/ToolbarButton'; import { PageToolbar } from './PageToolbar'; @@ -11,48 +9,46 @@ import { PageToolbar } from './PageToolbar'; const meta: Meta = { title: 'Navigation/Deprecated/PageToolbar', component: PageToolbar, - parameters: { - // TODO fix a11y issue in story and remove this - a11y: { test: 'off' }, - }, }; -export const Examples = () => { +export const WithNonClickableTitle = () => { return ( - - - - - Sync - - - - , - , - ]} - > - - - Sync - Settings - - - - action('Go back')}> - - - Discard - Apply - - - + + + Sync + + ); +}; + +export const WithClickableTitleAndParent = () => { + return ( + , + , + ]} + > + + + Sync + Settings + + ); +}; + +export const GoBackVersion = () => { + return ( + action('Go back')}> + + + Discard + Apply + ); }; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 67ce73c929b..a10057a5cc3 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { CSSProperties, ReactElement, ReactNode, useId, useState } from 'react'; +import { CSSProperties, PropsWithChildren, ReactElement, ReactNode, useId, useState } from 'react'; import * as React from 'react'; import { useMeasure, useToggle } from 'react-use'; @@ -110,6 +110,11 @@ interface HoverHeader { hoverHeaderOffset?: number; } +const MaybeWrap = ({ children }: PropsWithChildren<{}>) => { + const styles = useStyles2(getStyles); + return getFeatureToggle('preventPanelChromeOverflow') ?
{children}
: children; +}; + /** * @internal */ @@ -336,104 +341,106 @@ export function PanelChrome({ const hasHeaderContent = title || description || titleItems || menu || dragClass || actions; return ( - // tabIndex={0} is needed for keyboard accessibility in the plot area -
-
- {loadingState === LoadingState.Loading ? ( - - ) : null} -
- - {hoverHeader && ( - <> - {hasHeaderContent && ( - - {headerContent} - - )} - - {statusMessage && ( -
- -
- )} - - )} - - {hasHeader && ( -
- {statusMessage && ( -
- -
- )} - - {headerContent} - - {menu && ( - + {/* tabIndex={0} is needed for keyboard accessibility in the plot area */} +
+
+ {loadingState === LoadingState.Loading ? ( + - )} + ) : null}
- )} - {!collapsed && ( -
- {typeof children === 'function' ? children(innerWidth, innerHeight) : children} -
- )} -
+ {hoverHeader && ( + <> + {hasHeaderContent && ( + + {headerContent} + + )} + + {statusMessage && ( +
+ +
+ )} + + )} + + {hasHeader && ( +
+ {statusMessage && ( +
+ +
+ )} + + {headerContent} + + {menu && ( + + )} +
+ )} + + {!collapsed && ( +
+ {typeof children === 'function' ? children(innerWidth, innerHeight) : children} +
+ )} +
+ ); } @@ -494,10 +501,13 @@ const getStyles = (theme: GrafanaTheme2) => { return { container: css({ + position: 'relative', + }), + panel: css({ label: 'panel-container', backgroundColor: background, border: `1px solid ${borderColor}`, - position: 'relative', + position: getFeatureToggle('preventPanelChromeOverflow') ? 'unset' : 'relative', borderRadius: theme.shape.radius.default, height: '100%', display: 'flex', @@ -534,7 +544,7 @@ const getStyles = (theme: GrafanaTheme2) => { }, }, }), - transparentContainer: css({ + panelTransparent: css({ label: 'panel-transparent-container', backgroundColor: 'transparent', border: '1px solid transparent', diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.mdx b/packages/grafana-ui/src/components/Sidebar/Sidebar.mdx new file mode 100644 index 00000000000..e27c4aecd1b --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.mdx @@ -0,0 +1,5 @@ +import { Meta, ArgTypes } from '@storybook/blocks'; +import { Sidebar } from './Sidebar'; + +Sidebar +When to use diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx new file mode 100644 index 00000000000..8590f91a8b4 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx @@ -0,0 +1,211 @@ +import { css } from '@emotion/css'; +import { Meta, StoryFn } from '@storybook/react'; +import { useState } from 'react'; + +import { Button } from '../Button/Button'; +import { Box } from '../Layout/Box/Box'; + +import { Sidebar, SidebarPosition, useSidebar } from './Sidebar'; +import mdx from './Sidebar.mdx'; + +interface StoryProps { + position: SidebarPosition; +} + +const meta: Meta = { + title: 'Overlays/Sidebar', + parameters: { + docs: { + page: mdx, + }, + controls: {}, + }, + args: { + position: 'right', + }, + argTypes: { + position: { control: { type: 'radio' }, options: ['right', 'left'] }, + }, +}; + +export const Example: StoryFn = (args) => { + const [openPane, setOpenPane] = useState(''); + + const containerStyle = css({ + flexGrow: 1, + height: '600px', + display: 'flex', + flexDirection: 'column', + position: 'relative', + overflow: 'hidden', + }); + + const gridStyle = css({ + display: 'grid', + gridTemplateColumns: 'repeat(2, 1fr)', + gridAutoRows: '300px', + gap: '8px', + flexGrow: 1, + overflow: 'auto', + }); + + const togglePane = (pane: string) => { + if (openPane === pane) { + setOpenPane(''); + } else { + setOpenPane(pane); + } + }; + + const contextValue = useSidebar({ + hasOpenPane: !!openPane, + position: args.position, + bottomMargin: 0, + edgeMargin: 0, + }); + + return ( + +
+
+ {renderBox('A')} + {renderBox('B')} + {renderBox('C')} + {renderBox('D')} + {renderBox('E')} + {renderBox('F')} + {renderBox('G')} +
+ + {openPane === 'settings' && ( + + togglePane('')}> + + + + )} + {openPane === 'outline' && ( + + togglePane('')} /> + + )} + {openPane === 'add' && ( + + togglePane('')} /> + + )} + + togglePane('add')} + /> + + togglePane('settings')} + /> + togglePane('outline')} + /> + + + + + +
+
+ ); +}; + +export const VerticalTabs: StoryFn = (args) => { + const [openPane, setOpenPane] = useState('queries'); + + const togglePane = (pane: string) => { + setOpenPane(pane); + }; + + const containerStyle = css({ + flexGrow: 1, + height: '600px', + display: 'flex', + flexDirection: 'column', + position: 'relative', + overflow: 'hidden', + gap: '16px', + }); + + const vizWrapper = css({ + height: '30%', + display: 'flex', + }); + + const contextValue = useSidebar({ + position: args.position, + tabsMode: true, + edgeMargin: 0, + }); + + return ( + +
+
{renderBox('Visualization')}
+ + {openPane === 'queries' && ( + + + + )} + {openPane === 'transformations' && ( + + + + )} + + togglePane('queries')} + /> + togglePane('transformations')} + /> + + + +
+
+ ); +}; + +function renderBox(label: string) { + return ( + + {label} + + ); +} + +export default meta; diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx new file mode 100644 index 00000000000..187fbc90c4b --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react'; +import React, { act } from 'react'; + +import { Sidebar, useSidebar } from './Sidebar'; + +describe('Sidebar', () => { + it('should render sidebar', async () => { + render(); + + act(() => screen.getByLabelText('Settings').click()); + + // Verify pane is open + expect(screen.getByTestId('sidebar-pane-header-title')).toBeInTheDocument(); + + act(() => screen.getByLabelText('Dock').click()); + + // Verify wrapper pushes content when docked + const wrapper = screen.getByTestId('sidebar-test-wrapper'); + expect(wrapper).toHaveStyle('padding-right: 352px'); + + // Close pane + act(() => screen.getByLabelText('Close').click()); + // Verify pane is closed + expect(screen.queryByTestId('sidebar-pane-header-title')).not.toBeInTheDocument(); + }); +}); + +function TestSetup() { + const [openPane, setOpenPane] = React.useState(''); + const contextValue = useSidebar({ + position: 'right', + hasOpenPane: openPane !== '', + }); + + return ( +
+ + {openPane === 'settings' && ( + + setOpenPane('')} /> + + )} + + setOpenPane('settings')} /> + + + + +
+ ); +} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 00000000000..2d9b407a1ad --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,167 @@ +import { css, cx } from '@emotion/css'; +import { ReactNode, useContext } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; + +import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; + +import { SidebarButton } from './SidebarButton'; +import { SidebarPaneHeader } from './SidebarPaneHeader'; +import { SidebarResizer } from './SidebarResizer'; +import { SIDE_BAR_WIDTH_ICON_ONLY, SIDE_BAR_WIDTH_WITH_TEXT, SidebarContext, SidebarContextValue } from './useSidebar'; + +export interface Props { + children?: ReactNode; + contextValue: SidebarContextValue; +} + +export function SidebarComp({ children, contextValue }: Props) { + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const { isDocked, position, tabsMode, hasOpenPane, edgeMargin, bottomMargin } = contextValue; + + const className = cx({ + [styles.container]: true, + [styles.undockedPaneOpen]: hasOpenPane && !isDocked, + [styles.containerLeft]: position === 'left', + [styles.containerTabsMode]: tabsMode, + }); + + const style = { [position]: theme.spacing(edgeMargin), bottom: theme.spacing(bottomMargin) }; + + return ( + +
+ {!tabsMode && } + {children} +
+
+ ); +} + +export interface SiderbarToolbarProps { + children?: ReactNode; +} + +export function SiderbarToolbar({ children }: SiderbarToolbarProps) { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('Sidebar.Toolbar must be used within a Sidebar component'); + } + + return ( +
+ {children} +
+ {context.hasOpenPane && ( + + )} +
+ ); +} + +export function SidebarDivider() { + const styles = useStyles2(getStyles); + + return
; +} + +export interface SidebarOpenPaneProps { + children?: ReactNode; +} + +export function SidebarOpenPane({ children }: SidebarOpenPaneProps) { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('Sidebar.OpenPane must be used within a Sidebar component'); + } + + const className = cx(styles.openPane, context.position === 'right' ? styles.openPaneRight : styles.openPaneLeft); + + return ( +
+ {children} +
+ ); +} + +export const getStyles = (theme: GrafanaTheme2) => { + return { + container: css({ + display: 'flex', + position: 'absolute', + flexDirection: 'row', + flex: '1 1 0', + border: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.primary, + borderRadius: theme.shape.radius.default, + zIndex: theme.zIndex.navbarFixed, + bottom: 0, + top: 0, + right: 0, + }), + containerTabsMode: css({ + position: 'relative', + }), + containerLeft: css({ + right: 'unset', + flexDirection: 'row-reverse', + left: 0, + borderRadius: theme.shape.radius.default, + }), + undockedPaneOpen: css({ + boxShadow: theme.shadows.z3, + }), + toolbar: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: theme.spacing(1, 0), + flexGrow: 0, + gap: theme.spacing(1), + overflow: 'hidden', + width: theme.spacing(SIDE_BAR_WIDTH_WITH_TEXT), + }), + toolbarIconsOnly: css({ + width: theme.spacing(SIDE_BAR_WIDTH_ICON_ONLY), + }), + divider: css({ + height: '1px', + background: theme.colors.border.weak, + width: '100%', + }), + flexGrow: css({ + flexGrow: 1, + }), + openPane: css({ + width: '280px', + flexGrow: 1, + paddingBottom: theme.spacing(2), + }), + openPaneRight: css({ + borderRight: `1px solid ${theme.colors.border.weak}`, + }), + openPaneLeft: css({ + borderLeft: `1px solid ${theme.colors.border.weak}`, + }), + }; +}; + +export const Sidebar = Object.assign(SidebarComp, { + Toolbar: SiderbarToolbar, + Button: SidebarButton, + OpenPane: SidebarOpenPane, + Divider: SidebarDivider, + PaneHeader: SidebarPaneHeader, +}); + +export { type SidebarPosition, type SidebarContextValue, useSidebar } from './useSidebar'; diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx new file mode 100644 index 00000000000..f436d9630f6 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx @@ -0,0 +1,159 @@ +import { css, cx } from '@emotion/css'; +import { useContext } from 'react'; + +import { GrafanaTheme2, IconName, isIconName } from '@grafana/data'; + +import { useStyles2 } from '../../themes/ThemeContext'; +import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; +import { getActiveButtonStyles } from '../Button/Button'; +import { Icon } from '../Icon/Icon'; +import { Tooltip } from '../Tooltip/Tooltip'; + +import { SidebarContext } from './useSidebar'; + +export interface Props { + icon: IconName; + active?: boolean; + onClick?: () => void; + title: string; + tooltip?: string; +} + +export function SidebarButton({ icon, active, onClick, title, tooltip }: Props) { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('Sidebar.Button must be used within a Sidebar component'); + } + + const buttonClass = cx( + styles.button, + context.compact && styles.compact, + active && styles.active, + context.position === 'left' && styles.leftButton + ); + + return ( + + + + ); +} + +function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) { + if (!icon) { + return null; + } + + if (isIconName(icon)) { + return ; + } + + return icon; +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + button: css({ + label: 'toolbar-button', + position: 'relative', + display: 'flex', + flexDirection: 'column', + minHeight: theme.spacing(theme.components.height.md), + padding: theme.spacing(0, 1), + width: '100%', + overflow: 'hidden', + // borderRadius: theme.shape.radius.sm, + lineHeight: `${theme.components.height.md * theme.spacing.gridSize - 2}px`, + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.secondary, + background: 'transparent', + border: `none`, + + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background-color', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + }, + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + + '&:focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&[disabled], &:disabled': { + cursor: 'not-allowed', + opacity: theme.colors.action.disabledOpacity, + background: theme.colors.action.disabledBackground, + boxShadow: 'none', + + '&:hover': { + color: theme.colors.text.disabled, + background: theme.colors.action.disabledBackground, + boxShadow: 'none', + }, + }, + + '&:hover, &:focus-visible': { + color: theme.colors.text.primary, + background: theme.colors.action.hover, + }, + + '&:active': { + ...getActiveButtonStyles(theme.colors.secondary, 'solid'), + }, + }), + compact: css({ + height: theme.spacing(theme.components.height.md), + padding: theme.spacing(0, 1), + width: theme.spacing(5), + }), + active: css({ + color: theme.colors.text.primary, + background: theme.colors.action.selected, + '&::before': { + display: 'block', + content: '" "', + position: 'absolute', + right: 0, + top: 0, + height: '100%', + width: '2px', + borderRadius: theme.shape.radius.default, + backgroundImage: theme.colors.gradients.brandVertical, + }, + }), + buttonWrapper: css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + whiteSpace: 'nowrap', + }), + iconWrapper: css({}), + title: css({ + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + textOverflow: 'ellipsis', + overflow: 'hidden', + textAlign: 'center', + whiteSpace: 'nowrap', + }), + titleActive: css({ + color: theme.colors.text.primary, + }), + leftButton: css({ + '&::before': { + right: 'unset', + left: 0, + top: 0, + height: '100%', + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx new file mode 100644 index 00000000000..75393777879 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx @@ -0,0 +1,55 @@ +import { css } from '@emotion/css'; +import { ReactNode } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; + +import { useStyles2 } from '../../themes/ThemeContext'; +import { IconButton } from '../IconButton/IconButton'; +import { Text } from '../Text/Text'; + +export interface Props { + children?: ReactNode; + title: string; + onClose?: () => void; +} + +export function SidebarPaneHeader({ children, onClose, title }: Props) { + const styles = useStyles2(getStyles); + + return ( +
+ {onClose && ( + + )} + + {title} + +
+ {children} +
+ ); +} + +export const getStyles = (theme: GrafanaTheme2) => { + return { + wrapper: css({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1.5), + height: theme.spacing(6), + gap: theme.spacing(1), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + flexGrow: css({ + flexGrow: 1, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarResizer.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarResizer.tsx new file mode 100644 index 00000000000..7a243f00b54 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/SidebarResizer.tsx @@ -0,0 +1,93 @@ +import { css } from '@emotion/css'; +import { useCallback, useContext, useRef } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes/ThemeContext'; + +import { SidebarContext } from './useSidebar'; + +export function SidebarResizer() { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + const resizerRef = useRef(null); + const dragStart = useRef(null); + + if (!context) { + throw new Error('Sidebar.Resizer must be used within a Sidebar component'); + } + + const { onResize, position } = context; + + const onPointerDown = useCallback( + (e: React.PointerEvent) => { + if (resizerRef.current === null) { + return; + } + + resizerRef.current.setPointerCapture(e.pointerId); + dragStart.current = e.clientX; + }, + [resizerRef] + ); + + const onPointerMove = useCallback( + (e: React.PointerEvent) => { + if (dragStart.current === null) { + return; + } + + const diff = e.clientX - dragStart.current; + dragStart.current = e.clientX; + + onResize(position === 'right' ? -diff : diff); + }, + [dragStart, onResize, position] + ); + + const onPointerUp = useCallback( + (e: React.PointerEvent) => { + dragStart.current = null; + }, + [dragStart] + ); + + return ( +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + right: css({ + position: 'absolute', + width: theme.spacing.gridSize, + left: -theme.spacing.gridSize, + top: theme.shape.radius.default, + bottom: theme.shape.radius.default, + cursor: 'col-resize', + zIndex: 1, + '&:hover': { + borderRight: `1px solid ${theme.colors.primary.border}`, + }, + }), + left: css({ + position: 'absolute', + width: theme.spacing.gridSize, + right: -theme.spacing.gridSize, + top: theme.shape.radius.default, + bottom: theme.shape.radius.default, + cursor: 'col-resize', + zIndex: 1, + '&:hover': { + borderLeft: `1px solid ${theme.colors.primary.border}`, + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx new file mode 100644 index 00000000000..03d04b2f81a --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx @@ -0,0 +1,113 @@ +import { clamp } from 'lodash'; +import React, { useCallback } from 'react'; + +import { useTheme2 } from '../../themes/ThemeContext'; + +export type SidebarPosition = 'left' | 'right'; + +export interface SidebarContextValue { + isDocked: boolean; + position: SidebarPosition; + compact: boolean; + hasOpenPane?: boolean; + tabsMode?: boolean; + outerWrapperProps: React.HTMLAttributes; + paneWidth: number; + bottomMargin: number; + edgeMargin: number; + contentMargin: number; + onDockChange: () => void; + onResize: (diff: number) => void; +} + +export const SidebarContext: React.Context = React.createContext< + SidebarContextValue | undefined +>(undefined); + +export interface UseSideBarOptions { + hasOpenPane?: boolean; + position?: SidebarPosition; + tabsMode?: boolean; + compactDefault?: boolean; + /** defaults to 2 grid units (16px) */ + bottomMargin?: number; + /** defaults to 2 grid units (16px) */ + edgeMargin?: number; + /** defaults to 2 grid units (16px) */ + contentMargin?: number; +} + +export const SIDE_BAR_WIDTH_ICON_ONLY = 5; +export const SIDE_BAR_WIDTH_WITH_TEXT = 8; + +export function useSidebar({ + hasOpenPane, + position = 'right', + tabsMode, + compactDefault = true, + bottomMargin = 2, + edgeMargin = 2, + contentMargin = 2, +}: UseSideBarOptions): SidebarContextValue { + const theme = useTheme2(); + const [isDocked, setIsDocked] = React.useState(false); + const [paneWidth, setPaneWidth] = React.useState(280); + const [compact, setCompact] = React.useState(compactDefault); + // Used to accumulate drag distance to know when to change compact mode + const [_, setCompactDrag] = React.useState(0); + + const onDockChange = useCallback(() => setIsDocked((prev) => !prev), []); + + const prop = position === 'right' ? 'paddingRight' : 'paddingLeft'; + const toolbarWidth = + ((compact ? SIDE_BAR_WIDTH_ICON_ONLY : SIDE_BAR_WIDTH_WITH_TEXT) + edgeMargin + contentMargin) * + theme.spacing.gridSize; + + const outerWrapperProps = { + style: { + [prop]: isDocked && hasOpenPane ? paneWidth + toolbarWidth : toolbarWidth, + }, + }; + + const onResize = useCallback( + (diff: number) => { + setPaneWidth((prevWidth) => { + // If no pane is open we use the resize action to toggle compact mode (button text visibility) + if (!hasOpenPane) { + setCompactDrag((prevDrag) => { + const newDrag = prevDrag + diff; + if (newDrag < -20 && !compact) { + setCompact(true); + return 0; + } else if (newDrag > 20 && compact) { + setCompact(false); + return 0; + } + + return newDrag; + }); + + return prevWidth; + } + + return clamp(prevWidth + diff, 100, 500); + }); + }, + [hasOpenPane, compact] + ); + + return { + isDocked, + onDockChange, + onResize, + outerWrapperProps, + position, + compact, + hasOpenPane, + tabsMode, + paneWidth, + edgeMargin, + bottomMargin, + contentMargin, + }; +} diff --git a/packages/grafana-ui/src/components/Table/TableNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/Table/TableNG/__snapshots__/utils.test.ts.snap index 922b7514309..17707f4aa6c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/Table/TableNG/__snapshots__/utils.test.ts.snap @@ -94,9 +94,18 @@ exports[`TableNG utils buildInspectValue should handle string 1`] = ` ] `; +exports[`TableNG utils buildInspectValue should handle string w/ JSON (invalid JSON) 1`] = ` +[ + ""{\\"invalid\\": \\"json"", + "code", +] +`; + exports[`TableNG utils buildInspectValue should handle string w/ JSON 1`] = ` [ - ""baz"", + "{ + "valid": "json" +}", "code", ] `; diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts index 02527df8540..7d8c81961db 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -1619,6 +1619,7 @@ describe('TableNG utils', () => { }; const jsonStringField: Field = { ...stringField, + values: ['{"valid": "json"}', '{"invalid": "json', null, '{"another": "one"}'], config: { custom: { cellOptions: { type: TableCellDisplayMode.JSONView } } }, }; const booleanField: Field = { @@ -1675,11 +1676,12 @@ describe('TableNG utils', () => { it.each([ { name: 'numbers', input: { valueIdx: 0, field: numberFieldWithNulls } }, { name: 'string', input: { valueIdx: 0, field: stringField } }, - { name: 'string w/ JSON', input: { valueIdx: 2, field: jsonStringField } }, + { name: 'string w/ JSON', input: { valueIdx: 0, field: jsonStringField } }, + { name: 'string w/ JSON (invalid JSON)', input: { valueIdx: 1, field: jsonStringField } }, { name: 'boolean', input: { valueIdx: 0, field: booleanField } }, { name: 'NaN', input: { valueIdx: 4, field: numberFieldWithNulls } }, { name: 'null', input: { valueIdx: 3, field: numberFieldWithNulls } }, - { name: 'null w/ JSON', input: { valueIdx: 3, field: jsonStringField } }, + { name: 'null w/ JSON', input: { valueIdx: 2, field: jsonStringField } }, { name: 'undefined', input: { valueIdx: 6, field: numberFieldWithNulls } }, { name: 'sparkline', input: { valueIdx: 0, field: sparklineField } }, { name: 'sparkline (no x)', input: { valueIdx: 0, field: sparklineFieldNoX } }, diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index eb608ce2548..0226f8b6463 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -1049,7 +1049,15 @@ export function buildInspectValue(value: unknown, field: Field): [string, TableC inspectValue += ']'; mode = TableCellInspectorMode.code; } else if (cellOptions.type === TableCellDisplayMode.JSONView || Array.isArray(value) || isPlainObject(value)) { - inspectValue = JSON.stringify(value, null, ' '); + let toStringify = value; + if (typeof value === 'string') { + try { + toStringify = JSON.parse(value); + } catch { + // do nothing, toStringify will stay as the raw string + } + } + inspectValue = JSON.stringify(toStringify, null, ' '); mode = TableCellInspectorMode.code; } else { inspectValue = String(value ?? ''); diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx index b9c84ca8ec8..83f50757161 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx @@ -140,7 +140,7 @@ describe('Toggletip', () => { expect(onOpen).toHaveBeenCalledTimes(1); }); - it('should be able to focus toggletip content next in DOM order - forwards and backwards', async () => { + it('should trap content within the overlay', async () => { const onClose = jest.fn(); const afterInDom = 'Outside of toggletip'; @@ -164,17 +164,39 @@ describe('Toggletip', () => { const closeButton = screen.getByTestId('toggletip-header-close'); expect(closeButton).toHaveFocus(); - // focus after - await userEvent.tab(); - expect(afterButton).toHaveFocus(); + // tab forwards + await userEvent.keyboard('{tab}'); + // need to waitFor here to wait for the floating-ui focus manager to take effect + await waitFor(() => { + expect(closeButton).toHaveFocus(); + }); + expect(afterButton).not.toHaveFocus(); - // focus backwards - await userEvent.tab({ shift: true }); - expect(closeButton).toHaveFocus(); + // tab forwards again + await userEvent.keyboard('{tab}'); + // need to waitFor here to wait for the floating-ui focus manager to take effect + await waitFor(() => { + expect(closeButton).toHaveFocus(); + }); + expect(afterButton).not.toHaveFocus(); - // focus back to togglebutton - await userEvent.tab({ shift: true }); + // tab backwards + await userEvent.keyboard('{shift}{tab}'); + // need to waitFor here to wait for the floating-ui focus manager to take effect + await waitFor(() => { + expect(closeButton).toHaveFocus(); + }); + expect(afterButton).not.toHaveFocus(); + + // close overlay, focus back to togglebutton + await userEvent.keyboard('{escape}'); expect(button).toHaveFocus(); + expect(afterButton).not.toHaveFocus(); + + // tab forwards with overlay closed + await userEvent.tab(); + expect(closeButton).not.toHaveFocus(); + expect(afterButton).toHaveFocus(); }); describe('Focus state', () => { diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx index 84f13eaf112..41cc4e55ab4 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx @@ -20,6 +20,7 @@ import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; import { getPositioningMiddleware } from '../../utils/floating'; import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils'; import { IconButton } from '../IconButton/IconButton'; +import { Portal } from '../Portal/Portal'; import { ToggletipContent } from './types'; @@ -118,44 +119,46 @@ export const Toggletip = memo( ...getReferenceProps(), })} {isOpen && ( - -
- - {Boolean(title) &&
{title}
} - {closeButton && ( -
- { - setControlledVisible(false); - onClose?.(); - }} - /> + + +
+ + {Boolean(title) &&
{title}
} + {closeButton && ( +
+ { + setControlledVisible(false); + onClose?.(); + }} + /> +
+ )} +
+ {(typeof content === 'string' || isValidElement(content)) && content} + {typeof content === 'function' && content({})}
- )} -
- {(typeof content === 'string' || isValidElement(content)) && content} - {typeof content === 'function' && content({})} + {Boolean(footer) &&
{footer}
}
- {Boolean(footer) &&
{footer}
} -
-
+ +
)} ); diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx index 23c3426f69d..71ba96ba583 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx @@ -76,6 +76,7 @@ export const BasicWithIcon: StoryFn = (args) => { icon={args.icon} isOpen={args.isOpen} tooltip={args.tooltip} + aria-label="This is an aria-label" disabled={args.disabled} fullWidth={args.fullWidth} isHighlighted={args.isHighlighted} diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index 40b468d73fb..c1f99b0db93 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -1,5 +1,5 @@ import { cx, css } from '@emotion/css'; -import { forwardRef, ButtonHTMLAttributes } from 'react'; +import { forwardRef, ButtonHTMLAttributes, ReactNode } from 'react'; import * as React from 'react'; import { GrafanaTheme2, IconName, isIconName } from '@grafana/data'; @@ -12,7 +12,7 @@ import { getActiveButtonStyles, getPropertiesForVariant } from '../Button/Button import { Icon } from '../Icon/Icon'; import { Tooltip } from '../Tooltip/Tooltip'; -type CommonProps = { +interface BaseProps extends ButtonHTMLAttributes { /** Icon name */ icon?: IconName | React.ReactNode; /** Icon size */ @@ -35,9 +35,21 @@ type CommonProps = { iconOnly?: boolean; /** Show highlight dot */ isHighlighted?: boolean; -}; +} -export type ToolbarButtonProps = CommonProps & ButtonHTMLAttributes; +interface BasePropsWithChildren extends BaseProps { + children: ReactNode; +} + +interface BasePropsWithTooltip extends BaseProps { + tooltip: string; +} + +interface BasePropsWithAriaLabel extends BaseProps { + ['aria-label']: string; +} + +export type ToolbarButtonProps = BasePropsWithChildren | BasePropsWithTooltip | BasePropsWithAriaLabel; export type ToolbarButtonVariant = 'default' | 'primary' | 'destructive' | 'active' | 'canvas'; @@ -46,72 +58,68 @@ export type ToolbarButtonVariant = 'default' | 'primary' | 'destructive' | 'acti * * https://developers.grafana.com/ui/latest/index.html?path=/docs/navigation-toolbarbutton--docs */ -export const ToolbarButton = forwardRef( - ( +export const ToolbarButton = forwardRef((props, ref) => { + const styles = useStyles2(getStyles); + const { + tooltip, + icon, + iconSize, + className, + children, + imgSrc, + imgAlt, + fullWidth, + isOpen, + narrow, + variant = 'default', + iconOnly, + 'aria-label': ariaLabel, + isHighlighted, + ...rest + } = props; + + const buttonStyles = cx( { - tooltip, - icon, - iconSize, - className, - children, - imgSrc, - imgAlt, - fullWidth, - isOpen, - narrow, - variant = 'default', - iconOnly, - 'aria-label': ariaLabel, - isHighlighted, - ...rest + [styles.button]: true, + [styles.buttonFullWidth]: fullWidth, + [styles.narrow]: narrow, }, - ref - ) => { - const styles = useStyles2(getStyles); + styles[variant], + className + ); - const buttonStyles = cx( - { - [styles.button]: true, - [styles.buttonFullWidth]: fullWidth, - [styles.narrow]: narrow, - }, - styles[variant], - className - ); + const contentStyles = cx({ + [styles.content]: true, + [styles.contentWithIcon]: !!icon, + [styles.contentWithRightIcon]: isOpen !== undefined, + }); - const contentStyles = cx({ - [styles.content]: true, - [styles.contentWithIcon]: !!icon, - [styles.contentWithRightIcon]: isOpen !== undefined, - }); + const body = ( + + ); - const body = ( - - ); - - return tooltip ? ( - - {body} - - ) : ( - body - ); - } -); + return tooltip ? ( + + {body} + + ) : ( + body + ); +}); ToolbarButton.displayName = 'ToolbarButton'; diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index b46ce1d33bb..e567e3dd561 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -466,6 +466,7 @@ export { RunnerPlugin } from './slate-plugins/runner'; export { SelectionShortcutsPlugin } from './slate-plugins/selection_shortcuts'; export { SlatePrism, type Token } from './slate-plugins/slate-prism'; export { SuggestionsPlugin } from './slate-plugins/suggestions'; +export { Sidebar, useSidebar, type SidebarPosition, type SidebarContextValue } from './components/Sidebar/Sidebar'; // @deprecated import from @grafana/schema export { diff --git a/packages/grafana-ui/src/utils/tooltipUtils.ts b/packages/grafana-ui/src/utils/tooltipUtils.ts index c02edbece4c..eef90d24392 100644 --- a/packages/grafana-ui/src/utils/tooltipUtils.ts +++ b/packages/grafana-ui/src/utils/tooltipUtils.ts @@ -51,8 +51,8 @@ export function buildTooltipTheme( headerClose: css({ color: theme.colors.text.secondary, position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1.5), + right: theme.spacing(0.5), + top: theme.spacing(1), backgroundColor: 'transparent', border: 0, }), diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index b2ae6dc4df5..e822e4b6a62 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,7 +4,7 @@ go 1.25.3 require ( github.com/emicklei/go-restful/v3 v3.13.0 - github.com/grafana/grafana-plugin-sdk-go v0.282.0 + github.com/grafana/grafana-plugin-sdk-go v0.283.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -47,7 +47,6 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.1 // indirect @@ -90,7 +89,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/cobra v1.10.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 404c3743c70..8ab04c12ddf 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -101,8 +101,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grafana/grafana-plugin-sdk-go v0.282.0 h1:9JT9E6T0h8HsK9emTZ5s01uCKaf0Ok4PjYbb7M/iOjM= -github.com/grafana/grafana-plugin-sdk-go v0.282.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e h1:vheR6iPO1np+G/ARjcWx9yiWd7BnTDgyTgsnMhOvx70= @@ -208,8 +208,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= diff --git a/pkg/api/datasource/validation.go b/pkg/api/datasource/validation.go index f834cec4e1f..53e41a93ebf 100644 --- a/pkg/api/datasource/validation.go +++ b/pkg/api/datasource/validation.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/tsdb/mssql" ) var logger = log.New("datasource") @@ -71,7 +70,7 @@ func ValidateURL(typeName, urlStr string) (*url.URL, error) { var err error switch strings.ToLower(typeName) { case "mssql": - u, err = mssql.ParseURL(urlStr, logger) + u, err = parseURL(urlStr, logger) default: logger.Debug("Applying default URL parsing for this data source type", "type", typeName, "url", urlStr) @@ -90,3 +89,28 @@ func ValidateURL(typeName, urlStr string) (*url.URL, error) { return u, nil } + +type DebugOnlyLogger interface { + Debug(msg string, args ...interface{}) +} + +// ParseURL tries to parse an URL string into a URL object. +func parseURL(u string, logger DebugOnlyLogger) (*url.URL, error) { + logger.Debug("Parsing URL", "url", u) + + // Recognize ODBC connection strings like host\instance:1234 + reODBC := regexp.MustCompile(`^[^\\:]+(?:\\[^:]+)?(?::\d+)?(?:;.+)?$`) + var host string + switch { + case reODBC.MatchString(u): + logger.Debug("Recognized as ODBC URL format", "url", u) + host = u + default: + logger.Debug("Couldn't recognize as valid MSSQL URL", "url", u) + return nil, fmt.Errorf("unrecognized URL format: %q", u) + } + return &url.URL{ + Scheme: "sqlserver", + Host: host, + }, nil +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 2895fd14cf7..3554eb84244 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -637,6 +637,7 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin, Dependencies: plugin.Dependencies, ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin), Translations: plugin.Translations, + BuildMode: plugin.BuildMode, } if settings.Enabled { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 2d3bd11ab37..fa0b407aa2b 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -36,6 +36,7 @@ import ( "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" @@ -202,26 +203,27 @@ type HTTPServer struct { pluginsCDNService *pluginscdn.Service managedPluginsService managedplugins.Manager - userService user.Service - tempUserService tempUser.Service - loginAttemptService loginAttempt.Service - orgService org.Service - orgDeletionService org.DeletionService - TeamService team.Service - accesscontrolService accesscontrol.Service - annotationsRepo annotations.Repository - tagService tag.Service - oauthTokenService oauthtoken.OAuthTokenService - statsService stats.Service - authnService authn.Service - starApi *starApi.API - promRegister prometheus.Registerer - promGatherer prometheus.Gatherer - clientConfigProvider grafanaapiserver.DirectRestConfigProvider - namespacer request.NamespaceMapper - anonService anonymous.Service - userVerifier user.Verifier - tlsCerts TLSCerts + userService user.Service + tempUserService tempUser.Service + loginAttemptService loginAttempt.Service + orgService org.Service + orgDeletionService org.DeletionService + TeamService team.Service + accesscontrolService accesscontrol.Service + annotationsRepo annotations.Repository + tagService tag.Service + oauthTokenService oauthtoken.OAuthTokenService + statsService stats.Service + authnService authn.Service + starApi *starApi.API + promRegister prometheus.Registerer + promGatherer prometheus.Gatherer + clientConfigProvider grafanaapiserver.DirectRestConfigProvider + namespacer request.NamespaceMapper + anonService anonymous.Service + userVerifier user.Verifier + tlsCerts TLSCerts + htmlHandlerRequestsDuration *prometheus.HistogramVec } type TLSCerts struct { @@ -375,7 +377,15 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi namespacer: request.GetNamespaceMapper(cfg), anonService: anonService, userVerifier: userVerifier, + htmlHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "html_handler_requests_duration_seconds", + Help: "Duration of requests handled by the index.go HTML handler", + }, []string{"handler"}), } + + promRegister.MustRegister(hs.htmlHandlerRequestsDuration) + if hs.Listener != nil { hs.log.Debug("Using provided listener") } diff --git a/pkg/api/index.go b/pkg/api/index.go index 1f85f2b0dbe..afbebb3b013 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -8,11 +8,13 @@ import ( "html/template" "net/http" "strings" + "time" claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/webassets" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/middleware" ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -272,6 +274,11 @@ func (hs *HTTPServer) Index(c *contextmodel.ReqContext) { c, span := hs.injectSpan(c, "api.Index") defer span.End() + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.htmlHandlerRequestsDuration.WithLabelValues("index"), time.Since(start).Seconds()) + }() + data, err := hs.setIndexViewData(c) if err != nil { c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) @@ -286,6 +293,11 @@ func (hs *HTTPServer) NotFoundHandler(c *contextmodel.ReqContext) { return } + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.htmlHandlerRequestsDuration.WithLabelValues("not_found"), time.Since(start).Seconds()) + }() + data, err := hs.setIndexViewData(c) if err != nil { c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) diff --git a/pkg/api/login.go b/pkg/api/login.go index 60b894d0c49..89f452cd103 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -9,11 +9,13 @@ import ( "net/url" "regexp" "strings" + "time" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware/cookies" @@ -101,6 +103,10 @@ func (hs *HTTPServer) LoginView(c *contextmodel.ReqContext) { return } + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.htmlHandlerRequestsDuration.WithLabelValues("login"), time.Since(start).Seconds()) + }() viewData, err := setIndexViewData(hs, c) if err != nil { c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err) diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 2407876995e..59a539608a7 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/auth/authtest" @@ -44,6 +46,18 @@ import ( const loginCookieName = "grafana_session" +// setupHTMLHandlerMetrics creates and registers the prometheus metrics needed for HTTPServer tests +// that call methods using htmlHandlerRequestsDuration (LoginView, Index, NotFoundHandler) +func setupHTMLHandlerMetrics() (prometheus.Registerer, *prometheus.HistogramVec) { + promRegister := prometheus.NewRegistry() + htmlHandlerRequestsDuration := metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "html_handler_requests_duration_seconds", + }, []string{"handler"}) + promRegister.MustRegister(htmlHandlerRequestsDuration) + return promRegister, htmlHandlerRequestsDuration +} + func fakeSetIndexViewData(t *testing.T) { origSetIndexViewData := setIndexViewData t.Cleanup(func() { @@ -109,13 +123,16 @@ func TestLoginErrorCookieAPIEndpoint(t *testing.T) { sc := setupScenarioContext(t, "/login") cfg := setting.NewCfg() secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) + promRegister, htmlHandlerRequestsDuration := setupHTMLHandlerMetrics() hs := &HTTPServer{ - Cfg: cfg, - SettingsProvider: &setting.OSSImpl{Cfg: cfg}, - License: &licensing.OSSLicensingService{}, - SocialService: &mockSocialService{}, - SecretsService: secretsService, - Features: featuremgmt.WithFeatures(), + Cfg: cfg, + SettingsProvider: &setting.OSSImpl{Cfg: cfg}, + License: &licensing.OSSLicensingService{}, + SocialService: &mockSocialService{}, + SecretsService: secretsService, + Features: featuremgmt.WithFeatures(), + promRegister: promRegister, + htmlHandlerRequestsDuration: htmlHandlerRequestsDuration, } sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { @@ -156,13 +173,16 @@ func TestLoginViewRedirect(t *testing.T) { fakeViewIndex(t) sc := setupScenarioContext(t, "/login") cfg := setting.NewCfg() + promRegister, htmlHandlerRequestsDuration := setupHTMLHandlerMetrics() hs := &HTTPServer{ - Cfg: cfg, - SettingsProvider: &setting.OSSImpl{Cfg: cfg}, - License: &licensing.OSSLicensingService{}, - SocialService: &mockSocialService{}, - Features: featuremgmt.WithFeatures(), - log: log.NewNopLogger(), + Cfg: cfg, + SettingsProvider: &setting.OSSImpl{Cfg: cfg}, + License: &licensing.OSSLicensingService{}, + SocialService: &mockSocialService{}, + Features: featuremgmt.WithFeatures(), + log: log.NewNopLogger(), + promRegister: promRegister, + htmlHandlerRequestsDuration: htmlHandlerRequestsDuration, } hs.Cfg.CookieSecure = true @@ -492,13 +512,16 @@ func TestLoginOAuthRedirect(t *testing.T) { }, oAuthInfos: oAuthInfos, } + promRegister, htmlHandlerRequestsDuration := setupHTMLHandlerMetrics() hs := &HTTPServer{ - authnService: &authntest.FakeService{}, - Cfg: cfg, - SettingsProvider: &setting.OSSImpl{Cfg: cfg}, - License: &licensing.OSSLicensingService{}, - SocialService: mock, - Features: featuremgmt.WithFeatures(), + authnService: &authntest.FakeService{}, + Cfg: cfg, + SettingsProvider: &setting.OSSImpl{Cfg: cfg}, + License: &licensing.OSSLicensingService{}, + SocialService: mock, + Features: featuremgmt.WithFeatures(), + promRegister: promRegister, + htmlHandlerRequestsDuration: htmlHandlerRequestsDuration, } sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { @@ -521,11 +544,14 @@ func TestLoginInternal(t *testing.T) { fakeViewIndex(t) sc := setupScenarioContext(t, "/login") + promRegister, htmlHandlerRequestsDuration := setupHTMLHandlerMetrics() hs := &HTTPServer{ - Cfg: setting.NewCfg(), - License: &licensing.OSSLicensingService{}, - log: log.New("test"), - Features: featuremgmt.WithFeatures(), + Cfg: setting.NewCfg(), + License: &licensing.OSSLicensingService{}, + log: log.New("test"), + Features: featuremgmt.WithFeatures(), + promRegister: promRegister, + htmlHandlerRequestsDuration: htmlHandlerRequestsDuration, } sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { @@ -583,14 +609,17 @@ func TestAuthProxyLoginWithEnableLoginTokenAndEnabledOauthAutoLogin(t *testing.T sc := setupScenarioContext(t, "/login") sc.cfg.LoginCookieName = loginCookieName sc.cfg.OAuthAutoLogin = true + promRegister, htmlHandlerRequestsDuration := setupHTMLHandlerMetrics() hs := &HTTPServer{ - Cfg: sc.cfg, - SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, - License: &licensing.OSSLicensingService{}, - AuthTokenService: authtest.NewFakeUserAuthTokenService(), - log: log.New("hello"), - SocialService: mock, - Features: featuremgmt.WithFeatures(), + Cfg: sc.cfg, + SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, + License: &licensing.OSSLicensingService{}, + AuthTokenService: authtest.NewFakeUserAuthTokenService(), + log: log.New("hello"), + SocialService: mock, + Features: featuremgmt.WithFeatures(), + promRegister: promRegister, + htmlHandlerRequestsDuration: htmlHandlerRequestsDuration, } sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { @@ -623,14 +652,17 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte sc := setupScenarioContext(t, "/login") sc.cfg.LoginCookieName = loginCookieName + promRegister, htmlHandlerRequestsDuration := setupHTMLHandlerMetrics() hs := &HTTPServer{ - Cfg: sc.cfg, - SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, - License: &licensing.OSSLicensingService{}, - AuthTokenService: authtest.NewFakeUserAuthTokenService(), - log: log.New("hello"), - SocialService: &mockSocialService{}, - Features: featuremgmt.WithFeatures(), + Cfg: sc.cfg, + SettingsProvider: &setting.OSSImpl{Cfg: sc.cfg}, + License: &licensing.OSSLicensingService{}, + AuthTokenService: authtest.NewFakeUserAuthTokenService(), + log: log.New("hello"), + SocialService: &mockSocialService{}, + Features: featuremgmt.WithFeatures(), + promRegister: promRegister, + htmlHandlerRequestsDuration: htmlHandlerRequestsDuration, } sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index e22b8707834..99561233366 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -886,7 +886,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) { description: "Invalid ODBC URL", url: `localhost\instance::1433`, err: datasource.URLValidationError{ - Err: errors.New(`unrecognized MSSQL URL format: "localhost\\instance::1433"`), + Err: errors.New(`unrecognized URL format: "localhost\\instance::1433"`), URL: `localhost\instance::1433`, }, }, diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index 3d5b23a8fc9..72cd28de511 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -10,7 +10,7 @@ import ( "reflect" jsoniter "github.com/json-iterator/go" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/tracing" diff --git a/pkg/apimachinery/apis/common/v0alpha1/secure_values.go b/pkg/apimachinery/apis/common/v0alpha1/secure_values.go index fd10d9c32a7..1eff978fe8f 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/secure_values.go +++ b/pkg/apimachinery/apis/common/v0alpha1/secure_values.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" openapi "k8s.io/kube-openapi/pkg/common" spec "k8s.io/kube-openapi/pkg/validation/spec" ptr "k8s.io/utils/ptr" diff --git a/pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go b/pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go index 83dd8ec6762..fad96ec27ae 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go +++ b/pkg/apimachinery/apis/common/v0alpha1/secure_values_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "k8s.io/kube-openapi/pkg/validation/strfmt" "k8s.io/kube-openapi/pkg/validation/validate" diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 9543bd29c67..df34d602434 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,18 +3,17 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.25.3 require ( + github.com/go-jose/go-jose/v4 v4.1.2 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 // @grafana/identity-access-team + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team github.com/stretchr/testify v1.11.1 - gopkg.in/yaml.v3 v3.0.1 + go.yaml.in/yaml/v3 v3.0.4 k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 ) -require github.com/go-jose/go-jose/v4 v4.1.2 - require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect @@ -44,7 +43,6 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.44.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect @@ -54,6 +52,7 @@ require ( google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 1e3875d7db4..f5123c3038a 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -34,8 +34,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 00488ed9d85..6b39c1af046 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -158,7 +158,8 @@ var serviceIdentityTokenPermissions = []string{ "secret.grafana.app:*", "query.grafana.app:*", "iam.grafana.app:*", - "preferences.grafana.app:*", + "preferences.grafana.app:*", // user, team, and org preferences + "collections.grafana.app:*", // user stars // Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions. "secret.grafana.app/securevalues:decrypt", diff --git a/pkg/apis/query/v0alpha1/query_test.go b/pkg/apis/query/v0alpha1/query_test.go index 279373079ce..368ebe8dd9e 100644 --- a/pkg/apis/query/v0alpha1/query_test.go +++ b/pkg/apis/query/v0alpha1/query_test.go @@ -73,7 +73,7 @@ func TestParseQueriesIntoQueryDataRequest(t *testing.T) { "maxDataPoints": 10, "queryType": "foo", "refId": "Z", - "_timeRange": { + "timeRange": { "from": "100", "to": "200" } diff --git a/pkg/apiserver/endpoints/filters/path_rewriter_test.go b/pkg/apiserver/endpoints/filters/path_rewriter_test.go index 2340c3ff1f8..d8d8278ccf7 100644 --- a/pkg/apiserver/endpoints/filters/path_rewriter_test.go +++ b/pkg/apiserver/endpoints/filters/path_rewriter_test.go @@ -28,20 +28,20 @@ func Test_WithPathRewriters(t *testing.T) { handler := WithPathRewriters(mockHandler, rewriters) t.Run("should rewrite path", func(t *testing.T) { - req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stack-1234/query/blah", nil) + req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stacks-1234/query/blah", nil) assert.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, "/apis/scope.grafana.app/namespaces/stack-1234/query", rr.Body.String()) + assert.Equal(t, "/apis/scope.grafana.app/namespaces/stacks-1234/query", rr.Body.String()) }) t.Run("should ignore requests that don't match", func(t *testing.T) { - req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stack-1234/scopes/1", nil) + req, err := http.NewRequest("GET", "/apis/scope.grafana.app/namespaces/stacks-1234/scopes/1", nil) assert.NoError(t, err) rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Equal(t, "/apis/scope.grafana.app/namespaces/stack-1234/scopes/1", rr.Body.String()) + assert.Equal(t, "/apis/scope.grafana.app/namespaces/stacks-1234/scopes/1", rr.Body.String()) }) } diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index b1dba553ed5..6947fb20b67 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -4,7 +4,7 @@ go 1.25.3 require ( github.com/google/go-cmp v0.7.0 - github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 + github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 @@ -39,7 +39,6 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag/jsonname v0.25.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect @@ -64,7 +63,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 4070cf8e5f8..49b4ab91e93 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -67,8 +67,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o= github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37 h1:qEwZ+7MbPjzRvTi31iT9w7NBhKIpKwZrFbYmOZLqkwA= -github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY= +github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= @@ -129,8 +129,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index e5f3a2c93f2..e546d23b95b 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -339,6 +339,7 @@ type AppDTO struct { Dependencies Dependencies `json:"dependencies"` ModuleHash string `json:"moduleHash,omitempty"` Translations map[string]string `json:"translations,omitempty"` + BuildMode string `json:"buildMode,omitempty"` } const ( diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 16dd8d946f8..2c394dcc632 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -133,6 +133,9 @@ type JSONData struct { // List of languages supported by the plugin Languages []string `json:"languages,omitempty"` + + // Build mode of the plugin (set automatically at build time) + BuildMode string `json:"buildMode,omitempty"` } func ReadPluginJSON(reader io.Reader) (JSONData, error) { diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index e472abd1ad3..c60a20cee8d 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,11 +4,11 @@ go 1.25.3 require ( github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 - github.com/grafana/grafana-plugin-sdk-go v0.282.0 + github.com/grafana/grafana-plugin-sdk-go v0.283.0 github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.67.2 + github.com/prometheus/common v0.67.3 github.com/prometheus/prometheus v0.303.1 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/otel v1.38.0 diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 04556e32c24..f8c29e54262 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -130,8 +130,8 @@ github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81 github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI= github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4= -github.com/grafana/grafana-plugin-sdk-go v0.282.0 h1:9JT9E6T0h8HsK9emTZ5s01uCKaf0Ok4PjYbb7M/iOjM= -github.com/grafana/grafana-plugin-sdk-go v0.282.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134 h1:xly75v5lFNR37q+wXnwA5yU/fPW9IOSYbhFpt4tQyt8= github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134/go.mod h1:zijsUNa1zi476JJIR2Lcm/Paz1nRCno9XCp6hbS6G9o= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a h1:L7xgV9mP6MRF3L2/vDOjNR7heaBPbXPMGTDN9/jXSFQ= @@ -239,8 +239,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= -github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY= diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index a1d323f996a..10f1d7e52be 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -1,6 +1,7 @@ package apiregistry import ( + "github.com/grafana/grafana/pkg/registry/apis/collections" dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" @@ -27,6 +28,7 @@ func ProvideRegistryServiceSink( _ *query.QueryAPIBuilder, _ *userstorage.UserStorageAPIBuilder, _ *preferences.APIBuilder, + _ *collections.APIBuilder, _ *provisioning.APIBuilder, _ *ofrep.APIBuilder, _ *secret.DependencyRegisterer, diff --git a/pkg/registry/apis/preferences/admission.go b/pkg/registry/apis/collections/admission.go similarity index 79% rename from pkg/registry/apis/preferences/admission.go rename to pkg/registry/apis/collections/admission.go index 0703b37b8df..0811099b274 100644 --- a/pkg/registry/apis/preferences/admission.go +++ b/pkg/registry/apis/collections/admission.go @@ -1,4 +1,4 @@ -package preferences +package collections import ( "context" @@ -6,7 +6,7 @@ import ( "k8s.io/apiserver/pkg/admission" - preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" + collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" ) func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { @@ -24,7 +24,7 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis switch a.GetResource().Resource { case "stars": - stars, ok := obj.(*preferences.Stars) + stars, ok := obj.(*collections.Stars) if !ok { return fmt.Errorf("expected stars object: (%T)", obj) } diff --git a/pkg/registry/apis/collections/legacy/queries.go b/pkg/registry/apis/collections/legacy/queries.go new file mode 100644 index 00000000000..67153b6c07e --- /dev/null +++ b/pkg/registry/apis/collections/legacy/queries.go @@ -0,0 +1,67 @@ +package legacy + +import ( + "embed" + "fmt" + "text/template" + + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +// Templates setup. +var ( + //go:embed *.sql + sqlTemplatesFS embed.FS + + sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`)) +) + +func mustTemplate(filename string) *template.Template { + if t := sqlTemplates.Lookup(filename); t != nil { + return t + } + panic(fmt.Sprintf("template file not found: %s", filename)) +} + +// Templates. +var ( + sqlDashboardStarsQuery = mustTemplate("sql_dashboard_stars.sql") + sqlDashboardStarsRV = mustTemplate("sql_dashboard_stars_rv.sql") +) + +type starQuery struct { + sqltemplate.SQLTemplate + + OrgID int64 // >= 1 if UserID != "" + UserUID string + UserID int64 // for stars + QueryUIDs []string + QueryUID string + + StarTable string + UserTable string + QueryHistoryStarsTable string + QueryHistoryTable string +} + +func (r starQuery) Validate() error { + if r.UserUID != "" && r.OrgID < 1 { + return fmt.Errorf("requests with a userid, must include an orgID") + } + return nil +} + +func newStarQueryReq(sql *legacysql.LegacyDatabaseHelper, user string, orgId int64) starQuery { + return starQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + + UserUID: user, + OrgID: orgId, + + StarTable: sql.Table("star"), + UserTable: sql.Table("user"), + QueryHistoryStarsTable: sql.Table("query_history_star"), + QueryHistoryTable: sql.Table("query_history"), + } +} diff --git a/pkg/registry/apis/collections/legacy/queries_test.go b/pkg/registry/apis/collections/legacy/queries_test.go new file mode 100644 index 00000000000..2e94a8af564 --- /dev/null +++ b/pkg/registry/apis/collections/legacy/queries_test.go @@ -0,0 +1,52 @@ +package legacy + +import ( + "testing" + "text/template" + + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" +) + +func TestStarsQueries(t *testing.T) { + // prefix tables with grafana + nodb := &legacysql.LegacyDatabaseHelper{ + Table: func(n string) string { + return "grafana." + n + }, + } + + getStarQuery := func(orgId int64, user string) sqltemplate.SQLTemplate { + v := newStarQueryReq(nodb, user, orgId) + v.SQLTemplate = mocks.NewTestingSQLTemplate() + return &v + } + + mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ + RootDir: "testdata", + SQLTemplatesFS: sqlTemplatesFS, + Templates: map[*template.Template][]mocks.TemplateTestCase{ + sqlDashboardStarsQuery: { + { + Name: "all", + Data: getStarQuery(0, ""), + }, + { + Name: "org", + Data: getStarQuery(3, ""), + }, + { + Name: "user", + Data: getStarQuery(3, "abc"), + }, + }, + sqlDashboardStarsRV: { + { + Name: "get", + Data: getStarQuery(0, ""), + }, + }, + }, + }) +} diff --git a/pkg/registry/apis/collections/legacy/sql.go b/pkg/registry/apis/collections/legacy/sql.go new file mode 100644 index 00000000000..4f8ff3333a9 --- /dev/null +++ b/pkg/registry/apis/collections/legacy/sql.go @@ -0,0 +1,116 @@ +package legacy + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" +) + +type dashboardStars struct { + OrgID int64 + UserUID string + First int64 + Last int64 + + Dashboards []string +} + +type LegacySQL struct { + db legacysql.LegacyDatabaseProvider + startup time.Time +} + +func NewLegacySQL(db legacysql.LegacyDatabaseProvider) *LegacySQL { + return &LegacySQL{db: db, startup: time.Now()} +} + +// NOTE: this does not support paging -- lets check if that will be a problem in cloud +func (s *LegacySQL) getDashboardStars(ctx context.Context, orgId int64, user string) ([]dashboardStars, int64, error) { + var max sql.NullString + sql, err := s.db(ctx) + if err != nil { + return nil, 0, err + } + + req := newStarQueryReq(sql, user, orgId) + + q, err := sqltemplate.Execute(sqlDashboardStarsQuery, req) + if err != nil { + return nil, 0, fmt.Errorf("execute template %q: %w", sqlDashboardStarsQuery.Name(), err) + } + + sess := sql.DB.GetSqlxSession() + rows, err := sess.Query(ctx, q, req.GetArgs()...) + if err != nil { + return nil, 0, err + } + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + + stars := []dashboardStars{} + current := &dashboardStars{} + var orgID int64 + var userUID string + var dashboardUID string + var updated time.Time + + for rows.Next() { + err := rows.Scan(&orgID, &userUID, &dashboardUID, &updated) + if err != nil { + return nil, 0, err + } + + if orgID != current.OrgID || userUID != current.UserUID { + if current.UserUID != "" { + stars = append(stars, *current) + } + current = &dashboardStars{ + OrgID: orgID, + UserUID: userUID, + } + } + ts := updated.UnixMilli() + if ts > current.Last { + current.Last = ts + } + if ts < current.First || current.First == 0 { + current.First = ts + } + current.Dashboards = append(current.Dashboards, dashboardUID) + } + + // Add the last value + if current.UserUID != "" { + stars = append(stars, *current) + } + + // Find the RV unless it is a user query + if userUID == "" { + req.Reset() + q, err = sqltemplate.Execute(sqlDashboardStarsRV, req) + if err != nil { + return nil, 0, fmt.Errorf("execute template %q: %w", sqlDashboardStarsRV.Name(), err) + } + err = sess.Get(ctx, &max, q) + if err != nil { + return nil, 0, fmt.Errorf("unable to get RV %w", err) + } + if max.Valid && max.String != "" { + t, _ := time.Parse(time.RFC3339, max.String) + if !t.IsZero() { + updated = t + } + } else { + updated = s.startup + } + } + + return stars, updated.UnixMilli(), err +} diff --git a/pkg/registry/apis/preferences/legacy/sql_dashboard_stars.sql b/pkg/registry/apis/collections/legacy/sql_dashboard_stars.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/sql_dashboard_stars.sql rename to pkg/registry/apis/collections/legacy/sql_dashboard_stars.sql diff --git a/pkg/registry/apis/preferences/legacy/sql_dashboard_stars_rv.sql b/pkg/registry/apis/collections/legacy/sql_dashboard_stars_rv.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/sql_dashboard_stars_rv.sql rename to pkg/registry/apis/collections/legacy/sql_dashboard_stars_rv.sql diff --git a/pkg/registry/apis/preferences/legacy/stars.go b/pkg/registry/apis/collections/legacy/stars.go similarity index 78% rename from pkg/registry/apis/preferences/legacy/stars.go rename to pkg/registry/apis/collections/legacy/stars.go index f38f2fd786b..8c7d9e711ea 100644 --- a/pkg/registry/apis/preferences/legacy/stars.go +++ b/pkg/registry/apis/collections/legacy/stars.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "math/rand" - "slices" "strconv" "strings" "time" @@ -18,8 +17,8 @@ import ( "k8s.io/utils/ptr" authlib "github.com/grafana/authlib/types" + collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" dashboardsV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/registry/apis/preferences/utils" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -50,7 +49,7 @@ func NewDashboardStarsStorage( users: users, namespacer: namespacer, sql: sql, - tableConverter: preferences.StarsResourceInfo.TableConverter(), + tableConverter: collections.StarsResourceInfo.TableConverter(), } } @@ -63,7 +62,7 @@ type DashboardStarsStorage struct { } func (s *DashboardStarsStorage) New() runtime.Object { - return preferences.StarsKind().ZeroValue() + return collections.StarsKind().ZeroValue() } func (s *DashboardStarsStorage) Destroy() {} @@ -73,11 +72,11 @@ func (s *DashboardStarsStorage) NamespaceScoped() bool { } func (s *DashboardStarsStorage) GetSingularName() string { - return strings.ToLower(preferences.StarsKind().Kind()) + return strings.ToLower(collections.StarsKind().Kind()) } func (s *DashboardStarsStorage) NewList() runtime.Object { - return preferences.StarsKind().ZeroListValue() + return collections.StarsKind().ZeroListValue() } func (s *DashboardStarsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { @@ -104,18 +103,14 @@ func (s *DashboardStarsStorage) List(ctx context.Context, options *internalversi user = "" // can see everything } - list := &preferences.StarsList{} + list := &collections.StarsList{} found, rv, err := s.sql.getDashboardStars(ctx, ns.OrgID, user) if err != nil { return nil, err } - history, err := s.sql.getHistoryStars(ctx, ns.OrgID, "") - if err != nil { - return nil, err - } for _, v := range found { list.Items = append(list.Items, - asStarsResource(s.namespacer(v.OrgID), &v, history[v.UserUID])) + asStarsResource(s.namespacer(v.OrgID), &v)) } if rv > 0 { list.ResourceVersion = strconv.FormatInt(rv, 10) @@ -149,19 +144,14 @@ func (s *DashboardStarsStorage) Get(ctx context.Context, name string, options *m return nil, err } - history, err := s.sql.getHistoryStars(ctx, ns.OrgID, owner.Identifier) - if err != nil { - return nil, err - } - if len(found) == 0 || len(found[0].Dashboards) == 0 { - return nil, apiserrors.NewNotFound(preferences.StarsResourceInfo.GroupResource(), name) + return nil, apiserrors.NewNotFound(collections.StarsResourceInfo.GroupResource(), name) } - obj := asStarsResource(ns.Value, &found[0], history[owner.Identifier]) + obj := asStarsResource(ns.Value, &found[0]) return &obj, nil } -func getStars(stars *preferences.Stars, gk schema.GroupKind) []string { +func getStars(stars *collections.Stars, gk schema.GroupKind) []string { if stars == nil || len(stars.Spec.Resource) == 0 { return []string{} } @@ -174,7 +164,7 @@ func getStars(stars *preferences.Stars, gk schema.GroupKind) []string { } // Create implements rest.Creater. -func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Stars) (runtime.Object, error) { +func (s *DashboardStarsStorage) write(ctx context.Context, obj *collections.Stars) (runtime.Object, error) { ns, owner, err := getNamespaceAndOwner(ctx, obj.Name) if err != nil { return nil, err @@ -193,7 +183,7 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star stars := getStars(obj, schema.GroupKind{Group: "dashboard.grafana.app", Kind: "Dashboard"}) if len(stars) == 0 { err = s.stars.DeleteByUser(ctx, user.ID) - return &preferences.Stars{ObjectMeta: metav1.ObjectMeta{ + return &collections.Stars{ObjectMeta: metav1.ObjectMeta{ Name: obj.Name, Namespace: obj.Namespace, DeletionTimestamp: ptr.To(metav1.Now()), @@ -245,31 +235,6 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star changed = true } - // Apply history stars - stars = getStars(obj, schema.GroupKind{Group: "history.grafana.app", Kind: "Query"}) - res, err := s.sql.getHistoryStars(ctx, user.OrgID, user.UID) - if err != nil { - return nil, err - } - history := res[user.UID] - if !slices.Equal(stars, history) { - changed = true - if len(stars) == 0 { - err = s.sql.removeHistoryStar(ctx, user, nil) - if err != nil { - return nil, err - } - } else { - added, removed, _ := preferences.Changes(history, stars) - if len(removed) > 0 { - _ = s.sql.removeHistoryStar(ctx, user, nil) - } - for _, v := range added { - _ = s.sql.addHistoryStar(ctx, user, v) // one at a time so duplicates do not fail everything - } - } - } - if changed { return s.Get(ctx, obj.Name, &metav1.GetOptions{}) } @@ -278,7 +243,7 @@ func (s *DashboardStarsStorage) write(ctx context.Context, obj *preferences.Star // Create implements rest.Creater. func (s *DashboardStarsStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - stars, ok := obj.(*preferences.Stars) + stars, ok := obj.(*collections.Stars) if !ok { return nil, fmt.Errorf("expected stars object") } @@ -298,7 +263,7 @@ func (s *DashboardStarsStorage) Update(ctx context.Context, name string, objInfo return nil, false, err } - stars, ok := obj.(*preferences.Stars) + stars, ok := obj.(*collections.Stars) if !ok { return nil, false, fmt.Errorf("expected stars object") } @@ -309,7 +274,7 @@ func (s *DashboardStarsStorage) Update(ctx context.Context, name string, objInfo // Delete implements rest.GracefulDeleter. func (s *DashboardStarsStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - obj, err := s.write(ctx, &preferences.Stars{ObjectMeta: metav1.ObjectMeta{Name: name}}) + obj, err := s.write(ctx, &collections.Stars{ObjectMeta: metav1.ObjectMeta{Name: name}}) if err != nil { return nil, false, err } @@ -321,29 +286,22 @@ func (s *DashboardStarsStorage) DeleteCollection(ctx context.Context, deleteVali return nil, fmt.Errorf("not implemented yet") } -func asStarsResource(ns string, v *dashboardStars, history []string) preferences.Stars { - stars := preferences.Stars{ +func asStarsResource(ns string, v *dashboardStars) collections.Stars { + stars := collections.Stars{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("user-%s", v.UserUID), Namespace: ns, ResourceVersion: strconv.FormatInt(v.Last, 10), CreationTimestamp: metav1.NewTime(time.UnixMilli(v.First)), }, - Spec: preferences.StarsSpec{ - Resource: []preferences.StarsResource{{ + Spec: collections.StarsSpec{ + Resource: []collections.StarsResource{{ Group: dashboardsV1.APIGroup, Kind: "Dashboard", Names: v.Dashboards, }}, }, } - if len(history) > 0 { - stars.Spec.Resource = append(stars.Spec.Resource, preferences.StarsResource{ - Group: "history.grafana.app", - Kind: "Query", - Names: history, - }) - } stars.Spec.Normalize() return stars } diff --git a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars-all.sql b/pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars-all.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars-all.sql rename to pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars-all.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars-org.sql b/pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars-org.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars-org.sql rename to pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars-org.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars-user.sql b/pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars-user.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars-user.sql rename to pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars-user.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars_rv-get.sql b/pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars_rv-get.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/mysql--sql_dashboard_stars_rv-get.sql rename to pkg/registry/apis/collections/legacy/testdata/mysql--sql_dashboard_stars_rv-get.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars-all.sql b/pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars-all.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars-all.sql rename to pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars-all.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars-org.sql b/pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars-org.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars-org.sql rename to pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars-org.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars-user.sql b/pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars-user.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars-user.sql rename to pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars-user.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars_rv-get.sql b/pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars_rv-get.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/postgres--sql_dashboard_stars_rv-get.sql rename to pkg/registry/apis/collections/legacy/testdata/postgres--sql_dashboard_stars_rv-get.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars-all.sql b/pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars-all.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars-all.sql rename to pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars-all.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars-org.sql b/pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars-org.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars-org.sql rename to pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars-org.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars-user.sql b/pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars-user.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars-user.sql rename to pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars-user.sql diff --git a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars_rv-get.sql b/pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars_rv-get.sql similarity index 100% rename from pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_dashboard_stars_rv-get.sql rename to pkg/registry/apis/collections/legacy/testdata/sqlite--sql_dashboard_stars_rv-get.sql diff --git a/pkg/registry/apis/collections/register.go b/pkg/registry/apis/collections/register.go new file mode 100644 index 00000000000..9be4412c377 --- /dev/null +++ b/pkg/registry/apis/collections/register.go @@ -0,0 +1,178 @@ +package collections + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" + + collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" + grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/registry/apis/collections/legacy" + "github.com/grafana/grafana/pkg/registry/apis/preferences/utils" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/star" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/legacysql" +) + +var ( + _ builder.APIGroupBuilder = (*APIBuilder)(nil) + _ builder.APIGroupMutation = (*APIBuilder)(nil) +) + +type APIBuilder struct { + authorizer authorizer.Authorizer + legacyStars *legacy.DashboardStarsStorage +} + +func RegisterAPIService( + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, + db db.DB, + stars star.Service, + users user.Service, + apiregistration builder.APIRegistrar, +) *APIBuilder { + // Requires development settings and clearly experimental + //nolint:staticcheck // not yet migrated to OpenFeature + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + return nil + } + + sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db)) + builder := &APIBuilder{ + authorizer: &utils.AuthorizeFromName{ + Resource: map[string][]utils.ResourceOwner{ + "stars": {utils.UserResourceOwner}, + }, + }, + } + + namespacer := request.GetNamespaceMapper(cfg) + if stars != nil { + builder.legacyStars = legacy.NewDashboardStarsStorage(stars, users, namespacer, sql) + } + + apiregistration.RegisterAPI(builder) + return builder +} + +// AllowedV0Alpha1Resources implements builder.APIGroupBuilder. +func (b *APIBuilder) AllowedV0Alpha1Resources() []string { + return nil +} + +func (b *APIBuilder) GetGroupVersion() schema.GroupVersion { + return collections.GroupVersion +} + +func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error { + gv := collections.GroupVersion + err := collections.AddToScheme(scheme) + if err != nil { + return err + } + + metav1.AddToGroupVersion(scheme, gv) + return scheme.SetVersionPriority(gv) +} + +func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { + storage := map[string]rest.Storage{} + + // Configure Stars Dual writer + resource := collections.StarsResourceInfo + var stars grafanarest.Storage + stars, err := grafanaregistry.NewRegistryStore(opts.Scheme, resource, opts.OptsGetter) + if err != nil { + return err + } + stars = &starStorage{Storage: stars} // wrap List so we only return one value + if b.legacyStars != nil && opts.DualWriteBuilder != nil { + stars, err = opts.DualWriteBuilder(resource.GroupResource(), b.legacyStars, stars) + if err != nil { + return err + } + } + storage[resource.StoragePath()] = stars + storage[resource.StoragePath("update")] = &starsREST{store: stars} + + apiGroupInfo.VersionedResourcesStorageMap[collections.APIVersion] = storage + return nil +} + +func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer { + return b.authorizer +} + +func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return collections.GetOpenAPIDefinitions +} + +func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + oas.Info.Description = "Grafana collections" + + root := "/apis/" + b.GetGroupVersion().String() + "/" + updateKey := root + "namespaces/{namespace}/stars/{name}/update" + delete(oas.Paths.Paths, updateKey) + + // Add the group/kind/id properties to the path + stars, ok := oas.Paths.Paths[updateKey+"/{path}"] + if !ok || stars == nil { + return nil, fmt.Errorf("unable to find write path") + } + stars.Parameters = []*spec3.Parameter{ + stars.Parameters[0], // name + stars.Parameters[1], // namespace + { + ParameterProps: spec3.ParameterProps{ + Name: "group", + In: "path", + Example: "dashboard.grafana.app", + Description: "API group for stared item", + Schema: spec.StringProperty(), + Required: true, + }, + }, { + ParameterProps: spec3.ParameterProps{ + Name: "kind", + In: "path", + Example: "Dashboard", + Description: "Kind for stared item", + Schema: spec.StringProperty(), + Required: true, + }, + }, { + ParameterProps: spec3.ParameterProps{ + Name: "id", + In: "path", + Example: "", + Description: "The k8s name for the selected item", + Schema: spec.StringProperty(), + Required: true, + }, + }, + } + stars.Put.Description = "Add a starred item" + stars.Put.OperationId = "addStar" + stars.Delete.Description = "Remove a starred item" + stars.Delete.OperationId = "removeStar" + + delete(oas.Paths.Paths, updateKey+"/{path}") + oas.Paths.Paths[updateKey+"/{group}/{kind}/{id}"] = stars + + return oas, nil +} diff --git a/pkg/registry/apis/preferences/stars.go b/pkg/registry/apis/collections/stars.go similarity index 81% rename from pkg/registry/apis/preferences/stars.go rename to pkg/registry/apis/collections/stars.go index b5c8d0b0dcf..1fb16190d72 100644 --- a/pkg/registry/apis/preferences/stars.go +++ b/pkg/registry/apis/collections/stars.go @@ -1,4 +1,4 @@ -package preferences +package collections import ( "context" @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" authlib "github.com/grafana/authlib/types" - preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" + collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) @@ -32,12 +32,12 @@ func (s *starStorage) List(ctx context.Context, options *internalversion.ListOpt // Get the single user stars case authlib.TypeUser: - stars := &preferences.StarsList{} + stars := &collections.StarsList{} obj, _ := s.Get(ctx, "user-"+user.GetIdentifier(), &v1.GetOptions{}) if obj != nil { - s, ok := obj.(*preferences.Stars) + s, ok := obj.(*collections.Stars) if ok { - stars.Items = []preferences.Stars{*s} + stars.Items = []collections.Stars{*s} } } return stars, nil diff --git a/pkg/registry/apis/preferences/stars_update.go b/pkg/registry/apis/collections/stars_update.go similarity index 90% rename from pkg/registry/apis/preferences/stars_update.go rename to pkg/registry/apis/collections/stars_update.go index 2280a448e07..1a1de8c867c 100644 --- a/pkg/registry/apis/preferences/stars_update.go +++ b/pkg/registry/apis/collections/stars_update.go @@ -1,4 +1,4 @@ -package preferences +package collections import ( "context" @@ -11,7 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" + collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/registry/apis/preferences/utils" @@ -33,7 +33,7 @@ var ( ) func (r *starsREST) New() runtime.Object { - return &preferences.Stars{} + return &collections.Stars{} } func (r *starsREST) Destroy() {} @@ -47,7 +47,7 @@ func (r *starsREST) ProducesMIMETypes(verb string) []string { } func (r *starsREST) ProducesObject(verb string) interface{} { - return &preferences.Stars{} + return &collections.Stars{} } func (r *starsREST) NewConnectOptions() (runtime.Object, bool, string) { @@ -81,7 +81,7 @@ func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object, case "PUT": remove = false default: - responder.Error(apierrors.NewMethodNotSupported(preferences.PreferencesResourceInfo.GroupResource(), req.Method)) + responder.Error(apierrors.NewMethodNotSupported(collections.StarsResourceInfo.GroupResource(), req.Method)) return } @@ -94,7 +94,7 @@ func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object, }) return } - current = &preferences.Stars{ + current = &collections.Stars{ ObjectMeta: v1.ObjectMeta{ Name: name, Namespace: user.GetNamespace(), @@ -103,7 +103,7 @@ func (r *starsREST) Connect(ctx context.Context, name string, _ runtime.Object, } } - obj, ok := current.(*preferences.Stars) + obj, ok := current.(*collections.Stars) if !ok { responder.Error(fmt.Errorf("expected stars object")) return diff --git a/pkg/registry/apis/preferences/stars_update_test.go b/pkg/registry/apis/collections/stars_update_test.go similarity index 84% rename from pkg/registry/apis/preferences/stars_update_test.go rename to pkg/registry/apis/collections/stars_update_test.go index c4b58804cbc..aee38397ada 100644 --- a/pkg/registry/apis/preferences/stars_update_test.go +++ b/pkg/registry/apis/collections/stars_update_test.go @@ -1,4 +1,4 @@ -package preferences +package collections import ( "testing" @@ -16,7 +16,7 @@ func TestStarsWrite(t *testing.T) { err string }{{ name: "normal", - url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127", + url: "http://localhost:3000/apis/collections.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127", prefix: "/user-abc/write", item: starItem{ group: "dashboard.grafana.app", @@ -25,12 +25,12 @@ func TestStarsWrite(t *testing.T) { }, }, { name: "prefix not found", - url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127", + url: "http://localhost:3000/apis/collections.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/Dashboard/000000127", prefix: "/something/write", err: "invalid request path", }, { name: "missing three parts", - url: "http://localhost:3000/apis/preferences.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/000000127", + url: "http://localhost:3000/apis/collections.grafana.app/v1alpha1/namespaces/default/stars/user-abc/write/dashboard.grafana.app/000000127", prefix: "/user-abc/write", err: "expected {group}/{kind}/{id}", }} diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go index bdc47bf985a..e43a0fb7629 100644 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -34,9 +34,8 @@ type MigrateOptions struct { LargeObjects apistore.LargeObjectSupport BlobStore resourcepb.BlobStoreClient Resources []schema.GroupResource - WithHistory bool // only applies to dashboards - OnlyCount bool // just count the values - StackID string // stack identifier for logging + WithHistory bool // only applies to dashboards + OnlyCount bool // just count the values Progress func(count int, msg string) } @@ -140,7 +139,6 @@ func (a *dashboardSqlAccess) Migrate(ctx context.Context, opts MigrateOptions) ( // Now run each migration blobStore := BlobStoreInfo{} - opts.StackID = fmt.Sprintf("%d", info.StackID) // Pass stack ID through options a.log.Info("start migrating legacy resources", "namespace", opts.Namespace, "orgId", info.OrgID, "stackId", info.StackID) for _, m := range migratorFuncs { blobs, err := m(ctx, info.OrgID, opts, stream) @@ -318,7 +316,6 @@ func (a *dashboardSqlAccess) migrateDashboards(ctx context.Context, orgId int64, "uid", row.Dash.UID, "id", id, "version", row.Dash.Generation, - "stackId", opts.StackID, ) opts.Progress(-2, fmt.Sprintf("rejected: id:%s, uid:%s", id, row.Dash.Name)) } diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index d471cba752a..ec2f6053b95 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "github.com/grafana/grafana/pkg/services/featuremgmt" foldermodel "github.com/grafana/grafana/pkg/services/folder" @@ -130,6 +131,15 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) * Schema: spec.StringProperty(), }, }, + { + ParameterProps: spec3.ParameterProps{ + Name: "permission", + In: "query", + Description: "permission needed for the resource (View, Edit, Admin)", + Required: false, + Schema: spec.StringProperty().WithEnum("View", "Edit", "Admin"), + }, + }, { ParameterProps: spec3.ParameterProps{ Name: "sort", @@ -315,6 +325,15 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { } searchRequest.Fields = fields + switch strings.ToLower(queryParams.Get("permission")) { + case "edit": + searchRequest.Permission = int64(dashboardaccess.PERMISSION_EDIT) + case "view": + searchRequest.Permission = int64(dashboardaccess.PERMISSION_VIEW) + case "admin": + searchRequest.Permission = int64(dashboardaccess.PERMISSION_ADMIN) + } + // A search request can include multiple types, we need to acces the slice directly. types := queryParams["type"] hasDash := len(types) == 0 || slices.Contains(types, "dashboard") diff --git a/pkg/registry/apis/iam/externalgroupmapping/validate.go b/pkg/registry/apis/iam/externalgroupmapping/validate.go new file mode 100644 index 00000000000..851b77d8bd5 --- /dev/null +++ b/pkg/registry/apis/iam/externalgroupmapping/validate.go @@ -0,0 +1,22 @@ +package externalgroupmapping + +import ( + iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" +) + +func ValidateOnCreate(obj *iamv0alpha1.ExternalGroupMapping) error { + if obj == nil { + return apierrors.NewBadRequest("object must not be nil") + } + if obj.Spec.TeamRef.Name == "" { + return apierrors.NewBadRequest("teamRef.name is required") + } + + // FIXME: Add the ability to verify that the team exists in a follow up PR + + if obj.Spec.ExternalGroupId == "" { + return apierrors.NewBadRequest("externalGroupId is required") + } + return nil +} diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index e6b58f3c536..cc229f2ed4b 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -29,6 +29,7 @@ import ( grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage" "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" @@ -490,6 +491,8 @@ func (b *IdentityAccessManagementAPIBuilder) Validate(ctx context.Context, a adm return teambinding.ValidateOnCreate(ctx, typedObj) case *iamv0.ResourcePermission: return resourcepermission.ValidateCreateAndUpdateInput(ctx, typedObj) + case *iamv0.ExternalGroupMapping: + return externalgroupmapping.ValidateOnCreate(typedObj) } return nil case admission.Update: diff --git a/pkg/registry/apis/preferences/legacy/queries.go b/pkg/registry/apis/preferences/legacy/queries.go index d51904a0601..8ec075ec6de 100644 --- a/pkg/registry/apis/preferences/legacy/queries.go +++ b/pkg/registry/apis/preferences/legacy/queries.go @@ -26,52 +26,11 @@ func mustTemplate(filename string) *template.Template { // Templates. var ( - sqlDashboardStarsQuery = mustTemplate("sql_dashboard_stars.sql") - sqlDashboardStarsRV = mustTemplate("sql_dashboard_stars_rv.sql") - sqlHistoryStarsQuery = mustTemplate("sql_history_stars.sql") - sqlHistoryStarsInsert = mustTemplate("sql_history_stars_insert.sql") - sqlHistoryStarsDelete = mustTemplate("sql_history_stars_delete.sql") - sqlPreferencesQuery = mustTemplate("sql_preferences_query.sql") - sqlPreferencesRV = mustTemplate("sql_preferences_rv.sql") - sqlTeams = mustTemplate("sql_teams.sql") + sqlPreferencesQuery = mustTemplate("sql_preferences_query.sql") + sqlPreferencesRV = mustTemplate("sql_preferences_rv.sql") + sqlTeams = mustTemplate("sql_teams.sql") ) -type starQuery struct { - sqltemplate.SQLTemplate - - OrgID int64 // >= 1 if UserID != "" - UserUID string - UserID int64 // for stars - QueryUIDs []string - QueryUID string - - StarTable string - UserTable string - QueryHistoryStarsTable string - QueryHistoryTable string -} - -func (r starQuery) Validate() error { - if r.UserUID != "" && r.OrgID < 1 { - return fmt.Errorf("requests with a userid, must include an orgID") - } - return nil -} - -func newStarQueryReq(sql *legacysql.LegacyDatabaseHelper, user string, orgId int64) starQuery { - return starQuery{ - SQLTemplate: sqltemplate.New(sql.DialectForDriver()), - - UserUID: user, - OrgID: orgId, - - StarTable: sql.Table("star"), - UserTable: sql.Table("user"), - QueryHistoryStarsTable: sql.Table("query_history_star"), - QueryHistoryTable: sql.Table("query_history"), - } -} - type preferencesQuery struct { sqltemplate.SQLTemplate diff --git a/pkg/registry/apis/preferences/legacy/queries_test.go b/pkg/registry/apis/preferences/legacy/queries_test.go index 726049af7f5..8b4856a04dc 100644 --- a/pkg/registry/apis/preferences/legacy/queries_test.go +++ b/pkg/registry/apis/preferences/legacy/queries_test.go @@ -17,21 +17,6 @@ func TestStarsQueries(t *testing.T) { }, } - getStarQuery := func(orgId int64, user string) sqltemplate.SQLTemplate { - v := newStarQueryReq(nodb, user, orgId) - v.SQLTemplate = mocks.NewTestingSQLTemplate() - return &v - } - - getHistoryReq := func(orgId int64, userId int64, stars []string, star string) sqltemplate.SQLTemplate { - v := newStarQueryReq(nodb, "", orgId) - v.UserID = userId - v.QueryUIDs = stars - v.QueryUID = star - v.SQLTemplate = mocks.NewTestingSQLTemplate() - return &v - } - getPreferencesQuery := func(orgId int64, cb func(q *preferencesQuery)) sqltemplate.SQLTemplate { v := newPreferencesQueryReq(nodb, orgId) v.SQLTemplate = mocks.NewTestingSQLTemplate() @@ -49,56 +34,6 @@ func TestStarsQueries(t *testing.T) { RootDir: "testdata", SQLTemplatesFS: sqlTemplatesFS, Templates: map[*template.Template][]mocks.TemplateTestCase{ - sqlDashboardStarsQuery: { - { - Name: "all", - Data: getStarQuery(0, ""), - }, - { - Name: "org", - Data: getStarQuery(3, ""), - }, - { - Name: "user", - Data: getStarQuery(3, "abc"), - }, - }, - sqlDashboardStarsRV: { - { - Name: "get", - Data: getStarQuery(0, ""), - }, - }, - sqlHistoryStarsQuery: { - { - Name: "user", - Data: getStarQuery(1, "abc"), - }, - }, - sqlHistoryStarsQuery: { - { - Name: "org", - Data: getStarQuery(1, ""), - }, - }, - sqlHistoryStarsInsert: { - { - Name: "add star", - Data: getHistoryReq(1, 3, nil, "XXX"), - }, - }, - sqlHistoryStarsDelete: { - { - Name: "remove star", - Data: getHistoryReq(1, 3, []string{"xxx", "yyy"}, ""), - }, - }, - sqlHistoryStarsDelete: { - { - Name: "remove all star", - Data: getHistoryReq(1, 3, nil, ""), - }, - }, sqlPreferencesQuery: { { Name: "all", diff --git a/pkg/registry/apis/preferences/legacy/sql.go b/pkg/registry/apis/preferences/legacy/sql.go index 93f4ea39949..04cd7a3cf4f 100644 --- a/pkg/registry/apis/preferences/legacy/sql.go +++ b/pkg/registry/apis/preferences/legacy/sql.go @@ -12,20 +12,10 @@ import ( preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" pref "github.com/grafana/grafana/pkg/services/preference" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) -type dashboardStars struct { - OrgID int64 - UserUID string - First int64 - Last int64 - - Dashboards []string -} - type preferenceModel struct { ID int64 OrgID int64 @@ -49,177 +39,6 @@ func NewLegacySQL(db legacysql.LegacyDatabaseProvider) *LegacySQL { return &LegacySQL{db: db, startup: time.Now()} } -// NOTE: this does not support paging -- lets check if that will be a problem in cloud -func (s *LegacySQL) getDashboardStars(ctx context.Context, orgId int64, user string) ([]dashboardStars, int64, error) { - var max sql.NullString - sql, err := s.db(ctx) - if err != nil { - return nil, 0, err - } - - req := newStarQueryReq(sql, user, orgId) - - q, err := sqltemplate.Execute(sqlDashboardStarsQuery, req) - if err != nil { - return nil, 0, fmt.Errorf("execute template %q: %w", sqlDashboardStarsQuery.Name(), err) - } - - sess := sql.DB.GetSqlxSession() - rows, err := sess.Query(ctx, q, req.GetArgs()...) - if err != nil { - return nil, 0, err - } - defer func() { - if rows != nil { - _ = rows.Close() - } - }() - - stars := []dashboardStars{} - current := &dashboardStars{} - var orgID int64 - var userUID string - var dashboardUID string - var updated time.Time - - for rows.Next() { - err := rows.Scan(&orgID, &userUID, &dashboardUID, &updated) - if err != nil { - return nil, 0, err - } - - if orgID != current.OrgID || userUID != current.UserUID { - if current.UserUID != "" { - stars = append(stars, *current) - } - current = &dashboardStars{ - OrgID: orgID, - UserUID: userUID, - } - } - ts := updated.UnixMilli() - if ts > current.Last { - current.Last = ts - } - if ts < current.First || current.First == 0 { - current.First = ts - } - current.Dashboards = append(current.Dashboards, dashboardUID) - } - - // Add the last value - if current.UserUID != "" { - stars = append(stars, *current) - } - - // Find the RV unless it is a user query - if userUID == "" { - req.Reset() - q, err = sqltemplate.Execute(sqlDashboardStarsRV, req) - if err != nil { - return nil, 0, fmt.Errorf("execute template %q: %w", sqlPreferencesRV.Name(), err) - } - err = sess.Get(ctx, &max, q) - if err != nil { - return nil, 0, fmt.Errorf("unable to get RV %w", err) - } - if max.Valid && max.String != "" { - t, _ := time.Parse(time.RFC3339, max.String) - if !t.IsZero() { - updated = t - } - } else { - updated = s.startup - } - } - - return stars, updated.UnixMilli(), err -} - -func (s *LegacySQL) getHistoryStars(ctx context.Context, orgId int64, user string) (map[string][]string, error) { - sql, err := s.db(ctx) - if err != nil { - return nil, err - } - req := newStarQueryReq(sql, user, orgId) - - q, err := sqltemplate.Execute(sqlHistoryStarsQuery, req) - if err != nil { - return nil, fmt.Errorf("execute template %q: %w", sqlHistoryStarsQuery.Name(), err) - } - - sess := sql.DB.GetSqlxSession() - rows, err := sess.Query(ctx, q, req.GetArgs()...) - if err != nil { - return nil, err - } - defer func() { - if rows != nil { - _ = rows.Close() - } - }() - - last := user - res := make(map[string][]string) - buffer := make([]string, 0, 10) - var uid string - - for rows.Next() { - err := rows.Scan(&uid, &user) - if err != nil { - return nil, err - } - if user != last && len(buffer) > 0 { - res[last] = buffer - buffer = make([]string, 0, 10) - } - buffer = append(buffer, uid) - last = user - } - res[last] = buffer - return res, nil -} - -func (s *LegacySQL) removeHistoryStar(ctx context.Context, user *user.User, stars []string) error { - sql, err := s.db(ctx) - if err != nil { - return err - } - req := newStarQueryReq(sql, "", user.OrgID) - req.UserID = user.ID - if len(stars) > 0 { - req.QueryUIDs = stars - } - - q, err := sqltemplate.Execute(sqlHistoryStarsDelete, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlHistoryStarsDelete.Name(), err) - } - - sess := sql.DB.GetSqlxSession() - _, err = sess.Exec(ctx, q, req.GetArgs()...) - return err -} - -func (s *LegacySQL) addHistoryStar(ctx context.Context, user *user.User, star string) error { - sql, err := s.db(ctx) - if err != nil { - return err - } - req := newStarQueryReq(sql, "", user.OrgID) - req.UserID = user.ID - req.QueryUID = star - - q, err := sqltemplate.Execute(sqlHistoryStarsDelete, req) - if err != nil { - return fmt.Errorf("execute template %q: %w", sqlHistoryStarsDelete.Name(), err) - } - - sess := sql.DB.GetSqlxSession() - _, err = sess.Exec(ctx, q, req.GetArgs()...) - return err -} - // List all defined preferences in an org (valid for admin users only) func (s *LegacySQL) listPreferences(ctx context.Context, ns string, orgId int64, diff --git a/pkg/registry/apis/preferences/legacy/sql_history_stars.sql b/pkg/registry/apis/preferences/legacy/sql_history_stars.sql deleted file mode 100644 index 93f59103707..00000000000 --- a/pkg/registry/apis/preferences/legacy/sql_history_stars.sql +++ /dev/null @@ -1,9 +0,0 @@ -SELECT s.query_uid, u.uid as user_uid - FROM {{ .Ident .QueryHistoryStarsTable }} as s - JOIN {{ .Ident .QueryHistoryTable }} as h ON s.query_uid = h.uid - JOIN {{ .Ident .UserTable }} as u ON s.user_id = u.id - WHERE s.org_id = {{ .Arg .OrgID }} - {{ if .UserUID }} - AND u.uid = {{ .Arg .UserUID }} -{{ end }} - ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc diff --git a/pkg/registry/apis/preferences/legacy/sql_history_stars_delete.sql b/pkg/registry/apis/preferences/legacy/sql_history_stars_delete.sql deleted file mode 100644 index 4804740a42f..00000000000 --- a/pkg/registry/apis/preferences/legacy/sql_history_stars_delete.sql +++ /dev/null @@ -1,6 +0,0 @@ -DELETE FROM {{ .Ident .QueryHistoryStarsTable }} - WHERE org_id = {{ .Arg .OrgID }} - AND user_id = {{ .Arg .UserID }} - {{ if .QueryUIDs }} - AND query_uid IN ({{ .ArgList .QueryUIDs }}) -{{ end }} \ No newline at end of file diff --git a/pkg/registry/apis/preferences/legacy/sql_history_stars_insert.sql b/pkg/registry/apis/preferences/legacy/sql_history_stars_insert.sql deleted file mode 100644 index a85d14f2ed9..00000000000 --- a/pkg/registry/apis/preferences/legacy/sql_history_stars_insert.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT INTO {{ .Ident .QueryHistoryStarsTable }} - ( query_uid, user_id, org_id ) -VALUES - ( {{ .Arg .QueryUID }}, {{ .Arg .UserID }}, {{ .Arg .OrgID }} ) diff --git a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars-org.sql b/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars-org.sql deleted file mode 100755 index e651c6bc37c..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars-org.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT s.query_uid, u.uid as user_uid - FROM `grafana`.`query_history_star` as s - JOIN `grafana`.`query_history` as h ON s.query_uid = h.uid - JOIN `grafana`.`user` as u ON s.user_id = u.id - WHERE s.org_id = 1 - ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc diff --git a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars_delete-remove all star.sql b/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars_delete-remove all star.sql deleted file mode 100755 index 0c18ffdc0a8..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars_delete-remove all star.sql +++ /dev/null @@ -1,3 +0,0 @@ -DELETE FROM `grafana`.`query_history_star` - WHERE org_id = 1 - AND user_id = 3 diff --git a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars_insert-add star.sql b/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars_insert-add star.sql deleted file mode 100755 index 5a050187451..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/mysql--sql_history_stars_insert-add star.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT INTO `grafana`.`query_history_star` - ( query_uid, user_id, org_id ) -VALUES - ( 'XXX', 3, 1 ) diff --git a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars-org.sql b/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars-org.sql deleted file mode 100755 index d0fe6688c97..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars-org.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT s.query_uid, u.uid as user_uid - FROM "grafana"."query_history_star" as s - JOIN "grafana"."query_history" as h ON s.query_uid = h.uid - JOIN "grafana"."user" as u ON s.user_id = u.id - WHERE s.org_id = 1 - ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc diff --git a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars_delete-remove all star.sql b/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars_delete-remove all star.sql deleted file mode 100755 index 0b31729f72e..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars_delete-remove all star.sql +++ /dev/null @@ -1,3 +0,0 @@ -DELETE FROM "grafana"."query_history_star" - WHERE org_id = 1 - AND user_id = 3 diff --git a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars_insert-add star.sql b/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars_insert-add star.sql deleted file mode 100755 index d4729521f0d..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/postgres--sql_history_stars_insert-add star.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT INTO "grafana"."query_history_star" - ( query_uid, user_id, org_id ) -VALUES - ( 'XXX', 3, 1 ) diff --git a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars-org.sql b/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars-org.sql deleted file mode 100755 index d0fe6688c97..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars-org.sql +++ /dev/null @@ -1,6 +0,0 @@ -SELECT s.query_uid, u.uid as user_uid - FROM "grafana"."query_history_star" as s - JOIN "grafana"."query_history" as h ON s.query_uid = h.uid - JOIN "grafana"."user" as u ON s.user_id = u.id - WHERE s.org_id = 1 - ORDER BY s.org_id asc, s.user_id asc, s.query_uid asc diff --git a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars_delete-remove all star.sql b/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars_delete-remove all star.sql deleted file mode 100755 index 0b31729f72e..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars_delete-remove all star.sql +++ /dev/null @@ -1,3 +0,0 @@ -DELETE FROM "grafana"."query_history_star" - WHERE org_id = 1 - AND user_id = 3 diff --git a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars_insert-add star.sql b/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars_insert-add star.sql deleted file mode 100755 index d4729521f0d..00000000000 --- a/pkg/registry/apis/preferences/legacy/testdata/sqlite--sql_history_stars_insert-add star.sql +++ /dev/null @@ -1,4 +0,0 @@ -INSERT INTO "grafana"."query_history_star" - ( query_uid, user_id, org_id ) -VALUES - ( 'XXX', 3, 1 ) diff --git a/pkg/registry/apis/preferences/register.go b/pkg/registry/apis/preferences/register.go index a33cbed4b54..8964b0e0cd7 100644 --- a/pkg/registry/apis/preferences/register.go +++ b/pkg/registry/apis/preferences/register.go @@ -1,8 +1,6 @@ package preferences import ( - "fmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -10,12 +8,9 @@ import ( "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/kube-openapi/pkg/common" - "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" - grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/registry/apis/preferences/legacy" "github.com/grafana/grafana/pkg/registry/apis/preferences/utils" @@ -23,20 +18,17 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/featuremgmt" pref "github.com/grafana/grafana/pkg/services/preference" - "github.com/grafana/grafana/pkg/services/star" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql" ) var ( - _ builder.APIGroupBuilder = (*APIBuilder)(nil) - _ builder.APIGroupMutation = (*APIBuilder)(nil) + _ builder.APIGroupBuilder = (*APIBuilder)(nil) ) type APIBuilder struct { authorizer authorizer.Authorizer - legacyStars *legacy.DashboardStarsStorage legacyPrefs rest.Storage merger *merger // joins all preferences @@ -47,7 +39,6 @@ func RegisterAPIService( features featuremgmt.FeatureToggles, db db.DB, prefs pref.Service, - stars star.Service, users user.Service, apiregistration builder.APIRegistrar, ) *APIBuilder { @@ -60,11 +51,10 @@ func RegisterAPIService( sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db)) builder := &APIBuilder{ merger: newMerger(cfg, sql), - authorizer: &authorizeFromName{ - oknames: []string{"merged"}, - teams: sql, // should be from the IAM service - resource: map[string][]utils.ResourceOwner{ - "stars": {utils.UserResourceOwner}, + authorizer: &utils.AuthorizeFromName{ + OKNames: []string{"merged"}, + Teams: sql, // should be from the IAM service + Resource: map[string][]utils.ResourceOwner{ "preferences": { utils.NamespaceResourceOwner, utils.TeamResourceOwner, @@ -78,10 +68,6 @@ func RegisterAPIService( if prefs != nil { builder.legacyPrefs = legacy.NewPreferencesStorage(prefs, namespacer, sql) } - if stars != nil { - builder.legacyStars = legacy.NewDashboardStarsStorage(stars, users, namespacer, sql) - } - apiregistration.RegisterAPI(builder) return builder } @@ -109,24 +95,6 @@ func (b *APIBuilder) InstallSchema(scheme *runtime.Scheme) error { func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { storage := map[string]rest.Storage{} - // Configure Stars Dual writer - resource := preferences.StarsResourceInfo - var stars grafanarest.Storage - stars, err := grafanaregistry.NewRegistryStore(opts.Scheme, resource, opts.OptsGetter) - if err != nil { - return err - } - stars = &starStorage{Storage: stars} // wrap List so we only return one value - if b.legacyStars != nil && opts.DualWriteBuilder != nil { - stars, err = opts.DualWriteBuilder(resource.GroupResource(), b.legacyStars, stars) - if err != nil { - return err - } - } - storage[resource.StoragePath()] = stars - storage[resource.StoragePath("update")] = &starsREST{store: stars} - - // Configure Preferences prefs := preferences.PreferencesResourceInfo storage[prefs.StoragePath()] = b.legacyPrefs @@ -146,58 +114,3 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes { defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) return b.merger.GetAPIRoutes(defs) } - -func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { - oas.Info.Description = "Grafana preferences" - - root := "/apis/" + b.GetGroupVersion().String() + "/" - updateKey := root + "namespaces/{namespace}/stars/{name}/update" - delete(oas.Paths.Paths, updateKey) - - // Add the group/kind/id properties to the path - stars, ok := oas.Paths.Paths[updateKey+"/{path}"] - if !ok || stars == nil { - return nil, fmt.Errorf("unable to find write path") - } - stars.Parameters = []*spec3.Parameter{ - stars.Parameters[0], // name - stars.Parameters[1], // namespace - { - ParameterProps: spec3.ParameterProps{ - Name: "group", - In: "path", - Example: "dashboard.grafana.app", - Description: "API group for stared item", - Schema: spec.StringProperty(), - Required: true, - }, - }, { - ParameterProps: spec3.ParameterProps{ - Name: "kind", - In: "path", - Example: "Dashboard", - Description: "Kind for stared item", - Schema: spec.StringProperty(), - Required: true, - }, - }, { - ParameterProps: spec3.ParameterProps{ - Name: "id", - In: "path", - Example: "", - Description: "The k8s name for the selected item", - Schema: spec.StringProperty(), - Required: true, - }, - }, - } - stars.Put.Description = "Add a starred item" - stars.Put.OperationId = "addStar" - stars.Delete.Description = "Remove a starred item" - stars.Delete.OperationId = "removeStar" - - delete(oas.Paths.Paths, updateKey+"/{path}") - oas.Paths.Paths[updateKey+"/{group}/{kind}/{id}"] = stars - - return oas, nil -} diff --git a/pkg/registry/apis/preferences/authorizer.go b/pkg/registry/apis/preferences/utils/authorizer.go similarity index 78% rename from pkg/registry/apis/preferences/authorizer.go rename to pkg/registry/apis/preferences/utils/authorizer.go index 8eae3c4248f..a73e6e2c2ea 100644 --- a/pkg/registry/apis/preferences/authorizer.go +++ b/pkg/registry/apis/preferences/utils/authorizer.go @@ -1,4 +1,4 @@ -package preferences +package utils import ( "context" @@ -9,16 +9,15 @@ import ( "github.com/grafana/authlib/authz" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/registry/apis/preferences/utils" ) -type authorizeFromName struct { - teams utils.TeamService - oknames []string - resource map[string][]utils.ResourceOwner // may include unknown +type AuthorizeFromName struct { + Teams TeamService + OKNames []string + Resource map[string][]ResourceOwner // may include unknown } -func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { +func (a *AuthorizeFromName) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { user, err := identity.GetRequester(ctx) if err != nil || user == nil { return authorizer.DecisionDeny, "valid user is required", err @@ -28,7 +27,7 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri return authorizer.DecisionNoOpinion, "", nil } - owners, ok := a.resource[attr.GetResource()] + owners, ok := a.Resource[attr.GetResource()] if !ok { return authorizer.DecisionDeny, "missing resource name", nil } @@ -55,17 +54,17 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri } // the pseudo sub-resource - if a.oknames != nil && slices.Contains(a.oknames, attr.GetName()) { + if a.OKNames != nil && slices.Contains(a.OKNames, attr.GetName()) { return authorizer.DecisionAllow, "", nil } - info, _ := utils.ParseOwnerFromName(attr.GetName()) + info, _ := ParseOwnerFromName(attr.GetName()) if !slices.Contains(owners, info.Owner) { return authorizer.DecisionDeny, "unsupported owner type", nil } switch info.Owner { - case utils.NamespaceResourceOwner: + case NamespaceResourceOwner: if attr.IsReadOnly() { // Everyone can see the namespace return authorizer.DecisionAllow, "", nil @@ -75,17 +74,17 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri } return authorizer.DecisionDeny, "must be an org admin to edit", nil - case utils.UserResourceOwner: + case UserResourceOwner: if user.GetIdentifier() == info.Identifier { return authorizer.DecisionAllow, "", nil } return authorizer.DecisionDeny, "your are not the owner of the resource", nil - case utils.TeamResourceOwner: - if a.teams == nil { + case TeamResourceOwner: + if a.Teams == nil { return authorizer.DecisionDeny, "team checker not configured", err } - ok, err := a.teams.InTeam(ctx, user, info.Identifier, !attr.IsReadOnly()) + ok, err := a.Teams.InTeam(ctx, user, info.Identifier, !attr.IsReadOnly()) if err != nil { return authorizer.DecisionDeny, "error fetching teams", err } @@ -94,7 +93,7 @@ func (a *authorizeFromName) Authorize(ctx context.Context, attr authorizer.Attri } return authorizer.DecisionDeny, "you are not a member of the referenced team", nil - case utils.UnknownResourceOwner: + case UnknownResourceOwner: return authorizer.DecisionAllow, "", nil } diff --git a/pkg/registry/apis/preferences/authorizer_test.go b/pkg/registry/apis/preferences/utils/authorizer_test.go similarity index 90% rename from pkg/registry/apis/preferences/authorizer_test.go rename to pkg/registry/apis/preferences/utils/authorizer_test.go index 2b060bf6712..a1d10c24039 100644 --- a/pkg/registry/apis/preferences/authorizer_test.go +++ b/pkg/registry/apis/preferences/utils/authorizer_test.go @@ -1,4 +1,4 @@ -package preferences +package utils import ( "context" @@ -11,7 +11,6 @@ import ( "github.com/grafana/authlib/authn" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/registry/apis/preferences/utils" ) type expect struct { @@ -41,14 +40,14 @@ func TestAuthorizer_Authorize(t *testing.T) { tests := []struct { name string - teams func(t *testing.T) utils.TeamService - resource map[string][]utils.ResourceOwner + teams func(t *testing.T) TeamService + resource map[string][]ResourceOwner check []testCase }{ { name: "stars", - resource: map[string][]utils.ResourceOwner{ - "stars": {utils.UserResourceOwner}, + resource: map[string][]ResourceOwner{ + "stars": {UserResourceOwner}, }, check: []testCase{{ name: "matches user", @@ -80,9 +79,9 @@ func TestAuthorizer_Authorize(t *testing.T) { }}, }, { name: "fast path", - resource: map[string][]utils.ResourceOwner{ - "stars": {utils.UserResourceOwner}, - "preferences": {utils.TeamResourceOwner}, + resource: map[string][]ResourceOwner{ + "stars": {UserResourceOwner}, + "preferences": {TeamResourceOwner}, }, check: []testCase{{ name: "missing user", @@ -185,8 +184,8 @@ func TestAuthorizer_Authorize(t *testing.T) { }}, }, { name: "unknown owner", - resource: map[string][]utils.ResourceOwner{ - "stars": {utils.UnknownResourceOwner}, + resource: map[string][]ResourceOwner{ + "stars": {UnknownResourceOwner}, }, check: []testCase{{ name: "get", @@ -204,8 +203,8 @@ func TestAuthorizer_Authorize(t *testing.T) { }}, }, { name: "namespace", - resource: map[string][]utils.ResourceOwner{ - "ns": {utils.NamespaceResourceOwner}, + resource: map[string][]ResourceOwner{ + "ns": {NamespaceResourceOwner}, }, check: []testCase{{ name: "readonly", @@ -258,16 +257,16 @@ func TestAuthorizer_Authorize(t *testing.T) { }}, }, { name: "preferences teams", - teams: func(t *testing.T) utils.TeamService { - teams := utils.NewMockTeamService(t) + teams: func(t *testing.T) TeamService { + teams := NewMockTeamService(t) teams.On("InTeam", mock.Anything, userABC, "xyz", false).Return(true, nil) teams.On("InTeam", mock.Anything, userABC, "456", false).Return(false, nil) teams.On("InTeam", mock.Anything, userABC, "XXX", false).Return(true, fmt.Errorf("error from team")) return teams }, - resource: map[string][]utils.ResourceOwner{ + resource: map[string][]ResourceOwner{ "preferences": { - utils.TeamResourceOwner, + TeamResourceOwner, }, }, check: []testCase{{ @@ -318,11 +317,11 @@ func TestAuthorizer_Authorize(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - authz := &authorizeFromName{ - resource: tt.resource, + authz := &AuthorizeFromName{ + Resource: tt.resource, } if tt.teams != nil { - authz.teams = tt.teams(t) + authz.Teams = tt.teams(t) } for _, check := range tt.check { t.Run(check.name, func(t *testing.T) { diff --git a/pkg/registry/apis/provisioning/resources/parser.go b/pkg/registry/apis/provisioning/resources/parser.go index c8c65baf656..a684e4cffc5 100644 --- a/pkg/registry/apis/provisioning/resources/parser.go +++ b/pkg/registry/apis/provisioning/resources/parser.go @@ -8,7 +8,7 @@ import ( "path" "go.opentelemetry.io/otel/attribute" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -16,6 +16,7 @@ import ( "k8s.io/client-go/dynamic" "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/apps/provisioning/pkg/repository" diff --git a/pkg/registry/apis/provisioning/resources/repository_test.go b/pkg/registry/apis/provisioning/resources/repository_test.go index 5240d9d2758..cf845ba7440 100644 --- a/pkg/registry/apis/provisioning/resources/repository_test.go +++ b/pkg/registry/apis/provisioning/resources/repository_test.go @@ -536,7 +536,7 @@ func TestCheckResourceOwnership(t *testing.T) { "name": "test-resource", "annotations": map[string]interface{}{ utils.AnnoKeyManagerKind: "terraform", - utils.AnnoKeyManagerIdentity: "tf-stack-1", + utils.AnnoKeyManagerIdentity: "tf-stacks-1", }, }, }, @@ -546,7 +546,7 @@ func TestCheckResourceOwnership(t *testing.T) { Identity: "repo-1", }, expectError: true, - expectedMessage: "resource 'test-resource' is managed by terraform 'tf-stack-1' and cannot be modified by repo 'repo-1'", + expectedMessage: "resource 'test-resource' is managed by terraform 'tf-stacks-1' and cannot be modified by repo 'repo-1'", }, } diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index 58e22810c7b..5d76f64dfcd 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/collections" dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" @@ -63,6 +64,7 @@ var WireSet = wire.NewSet( service.RegisterAPIService, query.RegisterAPIService, preferences.RegisterAPIService, + collections.RegisterAPIService, userstorage.RegisterAPIService, ofrep.RegisterAPIService, ) diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index e80bcb720c9..5f997875985 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -48,6 +48,7 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl" "github.com/grafana/grafana/pkg/services/team/teamapi" "github.com/grafana/grafana/pkg/services/updatemanager" + unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations" ) func ProvideBackgroundServiceRegistry( @@ -73,6 +74,7 @@ func ProvideBackgroundServiceRegistry( dashboardServiceImpl *service.DashboardServiceImpl, secretsGarbageCollectionWorker *secretsgarbagecollectionworker.Worker, fixedRolesLoader *accesscontrol.FixedRolesLoader, + unifiedStorageMigrationProvider unifiedmigrations.UnifiedStorageMigrationProvider, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ serviceaccounts.Service, @@ -89,6 +91,7 @@ func ProvideBackgroundServiceRegistry( notifications, rendering, tokenService, + unifiedStorageMigrationProvider, provisioning, grafanaUpdateChecker, pluginsUpdateChecker, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 179b371760d..2fe6db16b31 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -181,6 +181,7 @@ import ( secretencryption "github.com/grafana/grafana/pkg/storage/secret/encryption" secretmetadata "github.com/grafana/grafana/pkg/storage/secret/metadata" secretmigrator "github.com/grafana/grafana/pkg/storage/secret/migrator" + unifiedmigrations "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" unifiedsearch "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" @@ -465,6 +466,8 @@ var wireBasicSet = wire.NewSet( // Unified storage resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, + unifiedmigrations.ProvideUnifiedStorageMigrationProvider, + wire.Bind(new(unifiedmigrations.UnifiedStorageMigrationProvider), new(*unifiedmigrations.UnifiedStorageMigrationProviderImpl)), // Kubernetes API server grafanaapiserver.WireSet, apiregistry.WireSet, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 6565d0e9062..91a1e50a224 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -48,6 +48,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/registry/apis" + "github.com/grafana/grafana/pkg/registry/apis/collections" "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" @@ -260,6 +261,7 @@ import ( "github.com/grafana/grafana/pkg/storage/secret/metadata" "github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/unified" + migrations3 "github.com/grafana/grafana/pkg/storage/unified/migrations" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/grafana/grafana/pkg/storage/unified/sql" @@ -836,6 +838,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api dashboardUpdater := service8.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) worker := garbagecollectionworker.ProvideWorker(cfg, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) fixedRolesLoader := accesscontrol.ProvideFixedRolesLoader(acimplService, featureToggles) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) + unifiedStorageMigrationProviderImpl := migrations3.ProvideUnifiedStorageMigrationProvider(legacyMigrator, cfg, resourceClient, sqlStore) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { return nil, err @@ -866,8 +870,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api return nil, err } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) - apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService) - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) + apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, userService, apiserverService) + collectionsAPIBuilder := collections.RegisterAPIService(cfg, featureToggles, sqlStore, starService, userService, apiserverService) webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder) pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) @@ -900,7 +904,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err @@ -916,7 +920,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokenService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationService) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, unifiedStorageMigrationProviderImpl, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, tracingService, featureToggles, registerer) if err != nil { @@ -1477,6 +1481,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac dashboardUpdater := service8.ProvideDashboardUpdater(inProcBus, pluginstoreService, service14, importDashboardService, service13, pluginService, dashboardService) worker := garbagecollectionworker.ProvideWorker(cfg, secureValueMetadataStorage, keeperMetadataStorage, ossKeeperService) fixedRolesLoader := accesscontrol.ProvideFixedRolesLoader(acimplService, featureToggles) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) + unifiedStorageMigrationProviderImpl := migrations3.ProvideUnifiedStorageMigrationProvider(legacyMigrator, cfg, resourceClient, sqlStore) healthService, err := grpcserver.ProvideHealthService(cfg, grpcserverProvider) if err != nil { return nil, err @@ -1507,8 +1513,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac return nil, err } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) - apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, starService, userService, apiserverService) - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) + apiBuilder := preferences.RegisterAPIService(cfg, featureToggles, sqlStore, prefService, userService, apiserverService) + collectionsAPIBuilder := collections.RegisterAPIService(cfg, featureToggles, sqlStore, starService, userService, apiserverService) webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) v3 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder) pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer) @@ -1541,7 +1547,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err @@ -1557,7 +1563,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } ossUserProtectionImpl := authinfoimpl.ProvideOSSUserProtectionService() registration := authnimpl.ProvideRegistration(cfg, authnService, orgService, userAuthTokenService, acimplService, permissionRegistry, apikeyService, userService, authService, ossUserProtectionImpl, loginattemptimplService, quotaService, authinfoimplService, renderingService, featureToggles, oauthtokentestService, socialService, remoteCache, ldapImpl, ossImpl, tracingService, tempuserService, notificationServiceMock) - backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) + backgroundServiceRegistry := backgroundsvcs.ProvideBackgroundServiceRegistry(httpServer, alertNG, cleanUpService, grafanaLive, gateway, notificationService, pluginstoreService, renderingService, userAuthTokenService, tracingService, provisioningServiceImpl, usageStats, statscollectorService, grafanaService, pluginsService, internalMetricsService, secretsService, remoteCache, storageService, searchService, entityEventsService, serviceAccountsService, grpcserverProvider, secretMigrationProviderImpl, loginattemptimplService, supportbundlesimplService, metricService, keyRetriever, angulardetectorsproviderDynamic, apiserverService, anonDeviceService, ssosettingsimplService, pluginexternalService, plugininstallerService, zanzanaReconciler, appregistryService, dashboardUpdater, dashboardServiceImpl, worker, fixedRolesLoader, unifiedStorageMigrationProviderImpl, serviceImpl, serviceAccountsProxy, healthService, reflectionService, apiService, apiregistryService, idimplService, teamAPI, ssosettingsimplService, cloudmigrationService, registration) usageStatsProvidersRegistry := usagestatssvcs.ProvideUsageStatsProvidersRegistry(acimplService, userService) server, err := New(opts, cfg, httpServer, acimplService, provisioningServiceImpl, backgroundServiceRegistry, usageStatsProvidersRegistry, statscollectorService, tracingService, featureToggles, registerer) if err != nil { @@ -1753,7 +1759,7 @@ var withOTelSet = wire.NewSet( otelTracer, grpcserver.ProvideService, interceptors.ProvideAuthenticator, ) -var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) +var wireBasicSet = wire.NewSet(annotationsimpl.ProvideService, wire.Bind(new(annotations.Repository), new(*annotationsimpl.RepositoryImpl)), New, api.ProvideHTTPServer, query.ProvideService, wire.Bind(new(query.Service), new(*query.ServiceImpl)), bus.ProvideBus, wire.Bind(new(bus.Bus), new(*bus.InProcBus)), rendering.ProvideService, wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), routing.ProvideRegister, wire.Bind(new(routing.RouteRegister), new(*routing.RouteRegisterImpl)), hooks.ProvideService, kvstore.ProvideService, localcache.ProvideService, bundleregistry.ProvideService, wire.Bind(new(supportbundles.Service), new(*bundleregistry.Service)), updatemanager.ProvideGrafanaService, updatemanager.ProvidePluginsService, service.ProvideService, wire.Bind(new(usagestats.Service), new(*service.UsageStats)), validator3.ProvideService, legacy.ProvideLegacyMigrator, pluginsintegration.WireSet, dashboards.ProvideFileStoreManager, wire.Bind(new(dashboards.FileStore), new(*dashboards.FileStoreManager)), cloudwatch.ProvideService, cloudmonitoring.ProvideService, azuremonitor.ProvideService, postgres.ProvideService, mysql.ProvideService, mssql.ProvideService, store.ProvideEntityEventsService, dualwrite.ProvideService, httpclientprovider.New, wire.Bind(new(httpclient.Provider), new(*httpclient2.Provider)), serverlock.ProvideService, wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)), annotationsimpl.ProvideCleanupService, wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)), cleanup.ProvideService, shorturlimpl.ProvideService, wire.Bind(new(shorturls.Service), new(*shorturlimpl.ShortURLService)), queryhistory.ProvideService, wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), correlations.ProvideService, wire.Bind(new(correlations.Service), new(*correlations.CorrelationsService)), quotaimpl.ProvideService, remotecache.ProvideService, wire.Bind(new(remotecache.CacheStorage), new(*remotecache.RemoteCache)), authinfoimpl.ProvideService, wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)), authinfoimpl.ProvideStore, datasourceproxy.ProvideService, sort.ProvideService, search2.ProvideService, searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, store.ProvideSystemUsersService, live.ProvideService, pushhttp.ProvideService, contexthandler.ProvideService, service12.ProvideService, wire.Bind(new(service12.LDAP), new(*service12.LDAPImpl)), jwt.ProvideService, wire.Bind(new(jwt.JWTService), new(*jwt.AuthService)), store2.ProvideDBStore, image.ProvideDeleteExpiredService, ngalert.ProvideService, librarypanels.ProvideService, wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), libraryelements.ProvideService, wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), notifications.ProvideService, notifications.ProvideSmtpService, github.ProvideFactory, tracing.ProvideService, tracing.ProvideTracingConfig, wire.Bind(new(tracing.Tracer), new(*tracing.TracingService)), withOTelSet, testdatasource.ProvideService, api4.ProvideService, opentsdb.ProvideService, socialimpl.ProvideService, influxdb.ProvideService, wire.Bind(new(social.Service), new(*socialimpl.SocialService)), tempo.ProvideService, loki.ProvideService, graphite.ProvideService, prometheus.ProvideService, elasticsearch.ProvideService, pyroscope.ProvideService, parca.ProvideService, zipkin.ProvideService, jaeger.ProvideService, service9.ProvideCacheService, wire.Bind(new(datasources.CacheService), new(*service9.CacheServiceImpl)), service2.ProvideEncryptionService, wire.Bind(new(encryption2.Internal), new(*service2.Service)), manager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*manager.SecretsService)), database.ProvideSecretsStore, wire.Bind(new(secrets.Store), new(*database.SecretsStoreImpl)), garbagecollectionworker.ProvideWorker, grafanads.ProvideService, wire.Bind(new(dashboardsnapshots.Store), new(*database5.DashboardSnapshotStore)), database5.ProvideStore, wire.Bind(new(dashboardsnapshots.Service), new(*service10.ServiceImpl)), service10.ProvideService, service9.ProvideService, wire.Bind(new(datasources.DataSourceService), new(*service9.Service)), service9.ProvideLegacyDataSourceLookup, retriever.ProvideService, wire.Bind(new(serviceaccounts.ServiceAccountRetriever), new(*retriever.Service)), ossaccesscontrol.ProvideServiceAccountPermissions, wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), manager3.ProvideServiceAccountsService, proxy.ProvideServiceAccountsProxy, wire.Bind(new(serviceaccounts.Service), new(*proxy.ServiceAccountsProxy)), dsquerierclient.NewNullQSDatasourceClientBuilder, expr.ProvideService, featuremgmt.ProvideManagerService, featuremgmt.ProvideToggles, service7.ProvideDashboardServiceImpl, wire.Bind(new(dashboards2.PermissionsRegistrationService), new(*service7.DashboardServiceImpl)), service7.ProvideDashboardService, service7.ProvideDashboardProvisioningService, service7.ProvideDashboardPluginService, database2.ProvideDashboardStore, folderimpl.ProvideService, wire.Bind(new(folder.Service), new(*folderimpl.Service)), wire.Bind(new(folder.LegacyService), new(*folderimpl.Service)), folderimpl.ProvideStore, wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), service11.ProvideService, wire.Bind(new(dashboardimport.Service), new(*service11.ImportDashboardService)), service8.ProvideService, wire.Bind(new(plugindashboards.Service), new(*service8.Service)), service8.ProvideDashboardUpdater, kvstore2.ProvideService, avatar.ProvideAvatarCacheServer, statscollector.ProvideService, csrf.ProvideCSRFFilter, wire.Bind(new(csrf.Service), new(*csrf.CSRF)), ossaccesscontrol.ProvideTeamPermissions, wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), ossaccesscontrol.ProvideFolderPermissions, wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ossaccesscontrol.ProvideReceiverPermissionsService, wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, dashverimpl.ProvideService, service3.ProvideService, wire.Bind(new(publicdashboards.Service), new(*service3.PublicDashboardServiceImpl)), database3.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*database3.PublicDashboardStoreImpl)), metric.ProvideService, api2.ProvideApi, api3.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, orgimpl.ProvideDeletionService, statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideHealthService, grpcserver.ProvideReflectionService, resolver.ProvideEntityReferenceResolver, teamimpl.ProvideService, teamapi.ProvideTeamAPI, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, wire.Bind(new(loginattempt.Service), new(*loginattemptimpl.Service)), migrations2.ProvideDataSourceMigrationService, migrations2.ProvideSecretMigrationProvider, wire.Bind(new(migrations2.SecretMigrationProvider), new(*migrations2.SecretMigrationProviderImpl)), promtypemigration.ProvideAzurePromMigrationService, promtypemigration.ProvideAmazonPromMigrationService, promtypemigration.ProvidePromTypeMigrationProvider, wire.Bind(new(promtypemigration.PromTypeMigrationProvider), new(*promtypemigration.PromTypeMigrationProviderImpl)), resourcepermissions.NewActionSetService, wire.Bind(new(accesscontrol.ActionResolver), new(resourcepermissions.ActionSetService)), wire.Bind(new(pluginaccesscontrol.ActionSetRegistry), new(resourcepermissions.ActionSetService)), permreg.ProvidePermissionRegistry, acimpl.ProvideAccessControl, accesscontrol.ProvideFixedRolesLoader, dualwrite2.ProvideZanzanaReconciler, navtreeimpl.ProvideService, wire.Bind(new(accesscontrol.AccessControl), new(*acimpl.AccessControl)), wire.Bind(new(notifications.TempUserStore), new(tempuser.Service)), tagimpl.ProvideService, wire.Bind(new(tag.Service), new(*tagimpl.Service)), authnimpl.ProvideService, authnimpl.ProvideIdentitySynchronizer, authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnServiceAuthenticateOnly, authnimpl.ProvideRegistration, supportbundlesimpl.ProvideService, extsvcaccounts.ProvideExtSvcAccountsService, wire.Bind(new(serviceaccounts.ExtSvcAccountsService), new(*extsvcaccounts.ExtSvcAccountsService)), registry2.ProvideExtSvcRegistry, wire.Bind(new(extsvcauth.ExternalServiceRegistry), new(*registry2.Registry)), anonstore.ProvideAnonDBStore, wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)), loggermw.Provide, slogadapter.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssosettingsimpl.ProvideService, wire.Bind(new(ssosettings.Service), new(*ssosettingsimpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrationimpl.ProvideService, caching.ProvideCachingServiceClient, userimpl.ProvideVerifier, connectors.ProvideOrgRoleMapper, wire.Bind(new(user.Verifier), new(*userimpl.Verifier)), authz.WireSet, metadata.ProvideSecureValueMetadataStorage, metadata.ProvideKeeperMetadataStorage, metadata.ProvideDecryptStorage, decrypt.ProvideDecryptAuthorizer, wire.Value([]decrypt.ExtraOwnerDecrypter(nil)), decrypt.ProvideDecryptService, inline.ProvideInlineSecureValueService, encryption.ProvideDataKeyStorage, encryption.ProvideGlobalDataKeyStorage, encryption.ProvideEncryptedValueStorage, encryption.ProvideGlobalEncryptedValueStorage, encryption.ProvideEncryptedValueMigrationExecutor, service5.ProvideSecureValueService, validator.ProvideKeeperValidator, validator.ProvideSecureValueValidator, mutator.ProvideKeeperMutator, mutator.ProvideSecureValueMutator, migrator.NewWithEngine, database4.ProvideDatabase, clock.ProvideClock, wire.Bind(new(contracts.Database), new(*database4.Database)), wire.Bind(new(contracts.Clock), new(*clock.Clock)), manager2.ProvideEncryptionManager, service4.ProvideAESGCMCipherService, resource.ProvideStorageMetrics, resource.ProvideIndexMetrics, migrations3.ProvideUnifiedStorageMigrationProvider, wire.Bind(new(migrations3.UnifiedStorageMigrationProvider), new(*migrations3.UnifiedStorageMigrationProviderImpl)), apiserver.WireSet, apiregistry.WireSet, appregistry.WireSet, client.ProvideK8sClientWithFallback) var wireSet = wire.NewSet( wireBasicSet, metrics.WireSet, sqlstore.ProvideService, metrics2.ProvideService, wire.Bind(new(notifications.Service), new(*notifications.NotificationService)), wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationService)), wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationService)), wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, oauthtoken.ProvideService, wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), wire.Bind(new(cleanup.AlertRuleService), new(*store2.DBstore)), diff --git a/pkg/services/apiserver/auth/authorizer/role.go b/pkg/services/apiserver/auth/authorizer/role.go index 6bc6e9d5474..82e80b38164 100644 --- a/pkg/services/apiserver/auth/authorizer/role.go +++ b/pkg/services/apiserver/auth/authorizer/role.go @@ -4,9 +4,10 @@ import ( "context" "fmt" + "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/org" - "k8s.io/apiserver/pkg/authorization/authorizer" ) var _ authorizer.Authorizer = &roleAuthorizer{} @@ -34,13 +35,15 @@ func (auth roleAuthorizer) Authorize(ctx context.Context, a authorizer.Attribute default: return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } - case org.RoleViewer, org.RoleNone: + case org.RoleViewer: switch a.GetVerb() { case "get", "list", "watch": return authorizer.DecisionAllow, "", nil default: return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } + case org.RoleNone: + return authorizer.DecisionDeny, errorMessageForGrafanaOrgRole(orgRole, a), nil } return authorizer.DecisionDeny, "", nil } diff --git a/pkg/services/apiserver/endpoints/request/namespace_test.go b/pkg/services/apiserver/endpoints/request/namespace_test.go index 6c565d43e19..47c75fa2e77 100644 --- a/pkg/services/apiserver/endpoints/request/namespace_test.go +++ b/pkg/services/apiserver/endpoints/request/namespace_test.go @@ -26,7 +26,7 @@ func TestNamespaceMapper(t *testing.T) { orgId: 123, expected: "org-123", }, - // an invalid use-case, but just documenting that it's handled as stack-0 + // an invalid use-case, but just documenting that it's handled as stacks-0 // this currently prevents the need to have the Mapper return (mapped, err) instead of just mapped. // err checking is avoided for now to keep the usage fluent { diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 48612981a45..88cf11fb83a 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -373,23 +373,10 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ExpectIssuer: "http://localhost:3000", }, }, - want: &authn.Identity{ - ID: "this-uid", - UID: "this-uid", - Name: "this-uid", - Type: claims.TypeAccessPolicy, - OrgID: 1, - AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet, - Namespace: "stack-1234", - AuthenticatedBy: "extendedjwt", - AuthID: "access-policy:this-uid", - ClientParams: authn.ClientParams{ - SyncPermissions: true, - }, - }, + wantErr: errExtJWTDisallowedNamespaceClaim, }, { - name: "should authenticate as user using specific deprecated namespace claim in access and id tokens", + name: "should NOT authenticate as user using specific deprecated namespace claim in access and id tokens", accessToken: &validAccessTokenClaimsWithDeprecatedStackClaimSet, idToken: &validIDTokenClaimsWithDeprecatedStackClaimSet, orgID: 1, @@ -401,20 +388,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { ExpectIssuer: "http://localhost:3000", }, }, - want: &authn.Identity{ - ID: "2", - Type: claims.TypeUser, - OrgID: 1, - AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet, - IDTokenClaims: &validIDTokenClaimsWithDeprecatedStackClaimSet, - Namespace: "stack-1234", - AuthenticatedBy: "extendedjwt", - AuthID: "access-policy:this-uid", - ClientParams: authn.ClientParams{ - SyncPermissions: true, - FetchSyncedUser: true, - }, - }, + wantErr: errExtJWTDisallowedNamespaceClaim, }, { name: "should authenticate as user using wildcard namespace for access token, setting allowed namespace to specific", diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index b65202b71d4..f7798cc365f 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -55,6 +55,8 @@ var ( errOAuthTokenExchange = errutil.Internal("auth.oauth.token.exchange", errutil.WithPublicMessage("Failed to get token from provider")) errOAuthUserInfo = errutil.Internal("auth.oauth.userinfo.error") + errOAuthMissingRefreshToken = errutil.Unauthorized("auth.oauth.token.refresh-token.missing", errutil.WithPublicMessage("Provider did not return a refresh token")) + errOAuthMissingRequiredEmail = errutil.Unauthorized("auth.oauth.email.missing", errutil.WithPublicMessage("Provider didn't return an email address")) errOAuthEmailNotAllowed = errutil.Unauthorized("auth.oauth.email.not-allowed", errutil.WithPublicMessage("Required email domain not fulfilled")) ) @@ -166,6 +168,15 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden } token.TokenType = "Bearer" + if oauthCfg.UseRefreshToken && token.RefreshToken == "" { + c.log.FromContext(ctx).Warn("No refresh token available with use_refresh_token enabled", "authmodule", c.moduleName) + + //nolint:staticcheck // not yet migrated to OpenFeature + if c.features.IsEnabledGlobally(featuremgmt.FlagRefreshTokenRequired) { + return nil, errOAuthMissingRefreshToken.Errorf("provider did not return a refresh token") + } + } + userInfo, err := connector.UserInfo(ctx, connector.Client(clientCtx, token), token) if err != nil { var sErr *connectors.SocialError diff --git a/pkg/services/authn/clients/oauth_test.go b/pkg/services/authn/clients/oauth_test.go index b6a9a07c488..324b956bd36 100644 --- a/pkg/services/authn/clients/oauth_test.go +++ b/pkg/services/authn/clients/oauth_test.go @@ -265,6 +265,63 @@ func TestOAuth_Authenticate(t *testing.T) { }, }, }, + { + desc: "should return error when no refresh token is available and feature toggle is enabled", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: mustParseURL("http://grafana.com/?state=some-state"), + }, + }, + oauthCfg: &social.OAuthInfo{UsePKCE: true, Enabled: true, UseRefreshToken: true}, + features: []any{featuremgmt.FlagRefreshTokenRequired}, + allowInsecureTakeover: true, + addStateCookie: true, + stateCookieValue: "some-state", + addPKCECookie: true, + pkceCookieValue: "some-pkce-value", + isEmailAllowed: true, + expectedErr: errOAuthMissingRefreshToken, + }, + { + desc: "should return identity when no refresh token is available and feature toggle is disabled", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{}, + URL: mustParseURL("http://grafana.com/?state=some-state"), + }, + }, + oauthCfg: &social.OAuthInfo{UsePKCE: true, Enabled: true, UseRefreshToken: true}, + addStateCookie: true, + stateCookieValue: "some-state", + addPKCECookie: true, + pkceCookieValue: "some-pkce-value", + isEmailAllowed: true, + userInfo: &social.BasicUserInfo{ + Id: "123", + Name: "name", + Email: "some@email.com", + Role: "Admin", + Groups: []string{"grp1", "grp2"}, + }, + expectedIdentity: &authn.Identity{ + Email: "some@email.com", + AuthenticatedBy: login.AzureADAuthModule, + AuthID: "123", + Name: "name", + Groups: []string{"grp1", "grp2"}, + OAuthToken: &oauth2.Token{}, + OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeams: true, + AllowSignUp: true, + FetchSyncedUser: true, + SyncOrgRoles: true, + LookUpParams: login.UserLookupParams{}, + }, + }, + }, } for _, tt := range tests { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 56f6fa585e0..615725df61d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -185,15 +185,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, }, - { - Name: "dataplaneFrontendFallback", - Description: "Support dataplane contract field name change for transformations and field name matchers where the name is different", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", - Owner: grafanaObservabilityMetricsSquad, - AllowSelfServe: true, - }, { Name: "disableSSEDataplane", Description: "Disables dataplane specific processing in server side expressions.", @@ -258,15 +249,6 @@ var ( Expression: "true", // enabled by default Owner: awsDatasourcesSquad, }, - { - Name: "transformationsRedesign", - Description: "Enables the transformations redesign", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", // enabled by default - Owner: grafanaObservabilityMetricsSquad, - AllowSelfServe: true, - }, { Name: "mlExpressions", Description: "Enable support for Machine Learning in server-side expressions", @@ -657,14 +639,6 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, }, - { - Name: "logRowsPopoverMenu", - Description: "Enable filtering menu displayed when text of a log line is selected", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", - Owner: grafanaObservabilityLogsSquad, - }, { Name: "tableSharedCrosshair", Description: "Enables shared crosshair in table panel", @@ -795,13 +769,6 @@ var ( FrontendOnly: true, Owner: grafanaDataProSquad, }, - { - Name: "newPDFRendering", - Description: "New implementation for the dashboard-to-PDF rendering", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOperatorExperienceSquad, - Expression: "true", // enabled by default, - }, { Name: "kubernetesAggregator", Description: "Enable grafana's embedded kube-aggregator", @@ -844,6 +811,14 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, + { + Name: "refreshTokenRequired", + Description: "Require that refresh tokens are present in oauth tokens.", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, + HideFromAdminPage: true, + }, { Name: "newDashboardWithFiltersAndGroupBy", Description: "Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering.", @@ -1317,13 +1292,6 @@ var ( FrontendOnly: true, Expression: "true", }, - { - Name: "feedbackButton", - Description: "Enables a button to send feedback from the Grafana UI", - Stage: FeatureStageExperimental, - Owner: grafanaOperatorExperienceSquad, - HideFromDocs: true, - }, { Name: "unifiedStorageSearchUI", Description: "Enable unified storage search UI", @@ -2073,6 +2041,18 @@ var ( FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "rudderstackUpgrade", + Description: "Enables the new version of rudderstack", + FrontendOnly: true, + Stage: FeatureStageExperimental, + Owner: grafanaFrontendPlatformSquad, + Expression: "false", + RequiresRestart: false, + AllowSelfServe: false, + HideFromDocs: false, + HideFromAdminPage: false, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 28b50a0840b..2b7f822673c 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -22,7 +22,6 @@ kubernetesStars,experimental,@grafana/grafana-app-platform-squad,false,true,fals influxqlStreamingParser,experimental,@grafana/partner-datasources,false,false,false influxdbRunQueriesInParallel,privatePreview,@grafana/partner-datasources,false,false,false lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false -dataplaneFrontendFallback,GA,@grafana/observability-metrics,false,false,true disableSSEDataplane,experimental,@grafana/grafana-datasources-core-services,false,false,false unifiedRequestLog,GA,@grafana/grafana-backend-group,false,false,false renderAuthJWT,preview,@grafana/grafana-operator-experience-squad,false,false,false @@ -32,7 +31,6 @@ enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true awsDatasourcesTempCredentials,GA,@grafana/aws-datasources,false,false,false -transformationsRedesign,GA,@grafana/observability-metrics,false,false,true mlExpressions,experimental,@grafana/alerting-squad,false,false,false datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false @@ -87,7 +85,6 @@ pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true timeComparison,experimental,@grafana/dataviz-squad,false,false,true logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true -logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false @@ -104,12 +101,12 @@ useMultipleScopeNodesEndpoint,experimental,@grafana/grafana-operator-experience- logQLScope,privatePreview,@grafana/observability-logs,false,false,false sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true -newPDFRendering,GA,@grafana/grafana-operator-experience-squad,false,false,false kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesAggregatorCapTokenAuth,experimental,@grafana/grafana-app-platform-squad,false,true,false groupByVariable,experimental,@grafana/dashboards-squad,false,false,false scopeFilters,experimental,@grafana/dashboards-squad,false,false,false oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false +refreshTokenRequired,experimental,@grafana/identity-access-team,false,false,false newDashboardWithFiltersAndGroupBy,experimental,@grafana/dashboards-squad,false,false,false cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false @@ -171,7 +168,6 @@ alertingEnrichmentPerRule,experimental,@grafana/alerting-squad,false,false,false alertingEnrichmentAssistantInvestigations,experimental,@grafana/alerting-squad,false,false,false alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,false,false alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true -feedbackButton,experimental,@grafana/grafana-operator-experience-squad,false,false,false unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false unifiedHistory,experimental,@grafana/grafana-search-navigate-organise,false,false,true @@ -266,3 +262,4 @@ kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false awsDatasourcesHttpProxy,experimental,@grafana/aws-datasources,false,false,false transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true ttlPluginInstanceManager,experimental,@grafana/plugins-platform-backend,false,false,true +rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f31bfc978bf..366e341c3a5 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -99,10 +99,6 @@ const ( // Changes logs responses from Loki to be compliant with the dataplane specification. FlagLokiLogsDataplane = "lokiLogsDataplane" - // FlagDataplaneFrontendFallback - // Support dataplane contract field name change for transformations and field name matchers where the name is different - FlagDataplaneFrontendFallback = "dataplaneFrontendFallback" - // FlagDisableSSEDataplane // Disables dataplane specific processing in server side expressions. FlagDisableSSEDataplane = "disableSSEDataplane" @@ -139,10 +135,6 @@ const ( // Support temporary security credentials in AWS plugins for Grafana Cloud customers FlagAwsDatasourcesTempCredentials = "awsDatasourcesTempCredentials" - // FlagTransformationsRedesign - // Enables the transformations redesign - FlagTransformationsRedesign = "transformationsRedesign" - // FlagMlExpressions // Enable support for Machine Learning in server-side expressions FlagMlExpressions = "mlExpressions" @@ -359,10 +351,6 @@ const ( // Enables infinite scrolling for the Logs panel in Explore and Dashboards FlagLogsInfiniteScrolling = "logsInfiniteScrolling" - // FlagLogRowsPopoverMenu - // Enable filtering menu displayed when text of a log line is selected - FlagLogRowsPopoverMenu = "logRowsPopoverMenu" - // FlagTableSharedCrosshair // Enables shared crosshair in table panel FlagTableSharedCrosshair = "tableSharedCrosshair" @@ -427,10 +415,6 @@ const ( // Enables column autocomplete for SQL Expressions FlagSqlExpressionsColumnAutoComplete = "sqlExpressionsColumnAutoComplete" - // FlagNewPDFRendering - // New implementation for the dashboard-to-PDF rendering - FlagNewPDFRendering = "newPDFRendering" - // FlagKubernetesAggregator // Enable grafana's embedded kube-aggregator FlagKubernetesAggregator = "kubernetesAggregator" @@ -451,6 +435,10 @@ const ( // Require that sub claims is present in oauth tokens. FlagOauthRequireSubClaim = "oauthRequireSubClaim" + // FlagRefreshTokenRequired + // Require that refresh tokens are present in oauth tokens. + FlagRefreshTokenRequired = "refreshTokenRequired" + // FlagNewDashboardWithFiltersAndGroupBy // Enables filters and group by variables on all new dashboards. Variables are added only if default data source supports filtering. FlagNewDashboardWithFiltersAndGroupBy = "newDashboardWithFiltersAndGroupBy" @@ -695,10 +683,6 @@ const ( // Enables simplified step mode in the notifications section FlagAlertingNotificationsStepMode = "alertingNotificationsStepMode" - // FlagFeedbackButton - // Enables a button to send feedback from the Grafana UI - FlagFeedbackButton = "feedbackButton" - // FlagUnifiedStorageSearchUI // Enable unified storage search UI FlagUnifiedStorageSearchUI = "unifiedStorageSearchUI" @@ -1073,4 +1057,8 @@ const ( // FlagTtlPluginInstanceManager // Enable TTL plugin instance manager FlagTtlPluginInstanceManager = "ttlPluginInstanceManager" + + // FlagRudderstackUpgrade + // Enables the new version of rudderstack + FlagRudderstackUpgrade = "rudderstackUpgrade" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 80db44fec7a..f352e01ca5e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1258,7 +1258,8 @@ "metadata": { "name": "dataplaneFrontendFallback", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-04-07T21:13:19Z" + "creationTimestamp": "2023-04-07T21:13:19Z", + "deletionTimestamp": "2025-11-06T17:39:31Z" }, "spec": { "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", @@ -1730,7 +1731,8 @@ "metadata": { "name": "feedbackButton", "resourceVersion": "1753448760331", - "creationTimestamp": "2024-12-02T17:08:15Z" + "creationTimestamp": "2024-12-02T17:08:15Z", + "deletionTimestamp": "2025-11-20T09:53:52Z" }, "spec": { "description": "Enables a button to send feedback from the Grafana UI", @@ -2517,7 +2519,8 @@ "metadata": { "name": "logRowsPopoverMenu", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-11-16T09:48:10Z" + "creationTimestamp": "2023-11-16T09:48:10Z", + "deletionTimestamp": "2025-11-07T10:57:27Z" }, "spec": { "description": "Enable filtering menu displayed when text of a log line is selected", @@ -2868,7 +2871,8 @@ "metadata": { "name": "newPDFRendering", "resourceVersion": "1753448760331", - "creationTimestamp": "2024-02-08T12:09:34Z" + "creationTimestamp": "2024-02-08T12:09:34Z", + "deletionTimestamp": "2025-11-14T14:39:41Z" }, "spec": { "description": "New implementation for the dashboard-to-PDF rendering", @@ -3541,6 +3545,20 @@ "hideFromAdminPage": true } }, + { + "metadata": { + "name": "refreshTokenRequired", + "resourceVersion": "1763561990273", + "creationTimestamp": "2025-11-19T14:19:50Z" + }, + "spec": { + "description": "Require that refresh tokens are present in oauth tokens.", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "regressionTransformation", @@ -3661,6 +3679,20 @@ "codeowner": "@grafana/identity-access-team" } }, + { + "metadata": { + "name": "rudderstackUpgrade", + "resourceVersion": "1763481217872", + "creationTimestamp": "2025-11-18T15:53:37Z" + }, + "spec": { + "description": "Enables the new version of rudderstack", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "savedQueries", @@ -4073,6 +4105,7 @@ "name": "timeRangePan", "resourceVersion": "1762290731154", "creationTimestamp": "2025-10-24T19:49:53Z", + "deletionTimestamp": "2025-11-06T17:39:31Z", "annotations": { "grafana.app/updatedTimestamp": "2025-11-04 21:12:11.154822 +0000 UTC" } @@ -4130,7 +4163,8 @@ "metadata": { "name": "transformationsRedesign", "resourceVersion": "1753448760331", - "creationTimestamp": "2023-07-12T16:35:49Z" + "creationTimestamp": "2023-07-12T16:35:49Z", + "deletionTimestamp": "2025-11-06T20:37:15Z" }, "spec": { "description": "Enables the transformations redesign", diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 93f3e5211f2..4d91eae3e5e 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -120,7 +120,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r Metrics: centrifuge.MetricsConfig{ MetricsNamespace: "grafana_live", }, - ClientQueueMaxSize: 4194304, // 4MB + ClientQueueMaxSize: cfg.LiveClientQueueMaxSize, // Use reasonably large expiration interval for stream meta key, // much bigger than maximum HistoryLifetime value in Node config. // This way stream meta data will expire, in some cases you may want diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index e59f912c972..7e999010795 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -15,7 +15,7 @@ import ( amconfig "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" prommodel "github.com/prometheus/common/model" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/errutil" diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 7ad8ee3c0b4..9371ca6a230 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -15,7 +15,7 @@ import ( prommodel "github.com/prometheus/common/model" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 7f1e9c8e4a2..2b8ef4b48e7 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -912,6 +912,39 @@ func TestRouteGetRuleStatuses(t *testing.T) { require.Len(t, result.Data.RuleGroups[0].Rules, 1) require.Equal(t, expectedRule.Title, result.Data.RuleGroups[0].Rules[0].Name) }) + + t.Run("should only return rules with given rule_uid list", func(t *testing.T) { + expectedRuleInGroup1 := rulesInGroup1[0] + expectedRuleInGroup3 := rulesInGroup3[1] + + r, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?rule_uid=%s&rule_uid=%s", expectedRuleInGroup1.UID, expectedRuleInGroup3.UID), nil) + require.NoError(t, err) + + c.Context = &web.Context{Req: r} + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + result := &apimodels.RuleResponse{} + require.NoError(t, json.Unmarshal(resp.Body(), result)) + + require.Len(t, result.Data.RuleGroups, 2) + require.True(t, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool { + return rg.Name == "rule-group-1" + })) + require.True(t, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool { + return rg.Name == "rule-group-3" + })) + require.Len(t, result.Data.RuleGroups[0].Rules, 1) + require.Len(t, result.Data.RuleGroups[1].Rules, 1) + + if result.Data.RuleGroups[0].Name == "rule-group-1" { + require.Equal(t, expectedRuleInGroup1.UID, result.Data.RuleGroups[0].Rules[0].UID) + require.Equal(t, expectedRuleInGroup3.UID, result.Data.RuleGroups[1].Rules[0].UID) + } else { + require.Equal(t, expectedRuleInGroup1.UID, result.Data.RuleGroups[1].Rules[0].UID) + require.Equal(t, expectedRuleInGroup3.UID, result.Data.RuleGroups[0].Rules[0].UID) + } + }) }) t.Run("when requesting rules with pagination", func(t *testing.T) { diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 830c2f83434..8eaf3ee6d93 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -2023,7 +2023,16 @@ func TestApiContactPointExportSnapshot(t *testing.T) { exportRaw, err := receiverExportResponses.ReadFile(p) require.NoError(t, err) - require.Equal(t, string(exportRaw), string(actualBody)) + switch tc.exportType { + case "json": + require.JSONEq(t, string(exportRaw), string(actualBody)) + case "yaml": + require.YAMLEq(t, string(exportRaw), string(actualBody)) + case "hcl": + require.Equal(t, string(exportRaw), string(actualBody)) + default: + t.Fatalf("unknown export type %q", tc.exportType) + } } t.Run("contact point export for all known configs", func(t *testing.T) { diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 8e368ac1e8e..268ed7b8c5d 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -385,14 +385,24 @@ func (srv RulerSrv) RouteGetRuleVersionsByUID(c *contextmodel.ReqContext, ruleUI } sort.Slice(rules, func(i, j int) bool { return rules[i].ID > rules[j].ID }) result := make(apimodels.GettableRuleVersions, 0, len(rules)) - userUIDmapping := srv.getUserUIDmapping(ctx, rules) + userUIDmapping := srv.getUserUIDmapping(ctx, alertRuleVersionsToAlertRules(rules)) for _, rule := range rules { // do not provide provenance status because we do not have historical changes for it - result = append(result, toGettableExtendedRuleNode(*rule, map[string]ngmodels.Provenance{}, userUIDmapping)) + ruleNode := toGettableExtendedRuleNode(rule.AlertRule, map[string]ngmodels.Provenance{}, userUIDmapping) + ruleNode.GrafanaManagedAlert.Message = rule.Message + result = append(result, ruleNode) } return response.JSON(http.StatusOK, result) } +func alertRuleVersionsToAlertRules(vs []*ngmodels.AlertRuleVersion) []*ngmodels.AlertRule { + result := make([]*ngmodels.AlertRule, len(vs)) + for i := range vs { + result[i] = &vs[i].AlertRule + } + return result +} + func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig, namespaceUID string) response.Response { var deletePermanently bool if c.QueryBool("deletePermanently") { diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index cb6a93b27b5..c6ddeee2a6c 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -560,11 +560,17 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], f) rule := gen.GenerateRef() - history := gen.With(gen.WithUID(rule.UID)).GenerateManyRef(3) + historyRules := gen.With(gen.WithUID(rule.UID)).GenerateManyRef(3) + history := make([]*models.AlertRuleVersion, len(historyRules)) // simulate order of the history rule.ID = 100 - for i, alertRule := range history { + for i, alertRule := range historyRules { alertRule.ID = rule.ID - int64(i) - 1 + + history[i] = &models.AlertRuleVersion{ + AlertRule: *alertRule, + Message: fmt.Sprintf("revision %d", i), + } } ruleStore.PutRule(context.Background(), rule) @@ -584,11 +590,22 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { require.Len(t, result, len(history)+1) // history + current version t.Run("should be in correct order", func(t *testing.T) { - expectedHistory := append([]*models.AlertRule{rule}, history...) + expectedHistory := append([]*models.AlertRuleVersion{{AlertRule: *rule}}, history...) for i, rul := range expectedHistory { assert.Equal(t, rul.UID, result[i].GrafanaManagedAlert.UID) } }) + + t.Run("should have correct messages", func(t *testing.T) { + expectedMessages := make([]string, 0, len(history)+1) + expectedMessages = append(expectedMessages, "") + for i := range history { + expectedMessages = append(expectedMessages, history[i].Message) + } + for i := range expectedMessages { + assert.Equal(t, expectedMessages[i], result[i].GrafanaManagedAlert.Message) + } + }) }) t.Run("NotFound when rule does not exist", func(t *testing.T) { @@ -599,10 +616,17 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { UID: "test", } guid := uuid.NewString() - history := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + historyRules := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + history := make([]*models.AlertRuleVersion, len(historyRules)) + for i, alertRule := range historyRules { + history[i] = &models.AlertRuleVersion{ + AlertRule: *alertRule, + Message: fmt.Sprintf("revision %d", i), + } + } ruleStore.History[guid] = append(ruleStore.History[guid], history...) // even if history is full of records - perms := createPermissionsForRules(history, orgID) + perms := createPermissionsForRules(historyRules, orgID) req := createRequestContextWithPerms(orgID, perms, nil) response := createService(ruleStore, nil).RouteGetRuleVersionsByUID(req, ruleKey.UID) @@ -643,10 +667,17 @@ func TestRouteGetRuleVersionsByUID(t *testing.T) { guid := uuid.NewString() rule := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey), gen.WithNamespaceUID(anotherFolder.UID)).GenerateRef() ruleStore.PutRule(context.Background(), rule) - history := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + historyRules := gen.With(gen.WithGUID(guid), gen.WithKey(ruleKey)).GenerateManyRef(3) + history := make([]*models.AlertRuleVersion, len(historyRules)) + for i, alertRule := range historyRules { + history[i] = &models.AlertRuleVersion{ + AlertRule: *alertRule, + Message: fmt.Sprintf("revision %d", i), + } + } ruleStore.History[guid] = history - perms := createPermissionsForRules(history, orgID) // grant permissions to all records in history but not the rule itself + perms := createPermissionsForRules(historyRules, orgID) // grant permissions to all records in history but not the rule itself req := createRequestContextWithPerms(orgID, perms, nil) response := createService(ruleStore, nil).RouteGetRuleVersionsByUID(req, ruleKey.UID) diff --git a/pkg/services/ngalert/api/lotex_am.go b/pkg/services/ngalert/api/lotex_am.go index 3d1745f376f..47900d336fb 100644 --- a/pkg/services/ngalert/api/lotex_am.go +++ b/pkg/services/ngalert/api/lotex_am.go @@ -8,7 +8,7 @@ import ( "io" "net/http" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/ngalert/api/lotex_ruler.go b/pkg/services/ngalert/api/lotex_ruler.go index d3dedfe2866..c058a554570 100644 --- a/pkg/services/ngalert/api/lotex_ruler.go +++ b/pkg/services/ngalert/api/lotex_ruler.go @@ -7,7 +7,7 @@ import ( "net/http" "net/url" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/ngalert/api/persist.go b/pkg/services/ngalert/api/persist.go index 52c4ca59a14..9e7df81f48e 100644 --- a/pkg/services/ngalert/api/persist.go +++ b/pkg/services/ngalert/api/persist.go @@ -35,6 +35,6 @@ type RuleStore interface { // IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace uids IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error) - GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRule, error) + GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRuleVersion, error) accesscontrol.RuleUIDToNamespaceStore } diff --git a/pkg/services/ngalert/api/prometheus/api_prometheus.go b/pkg/services/ngalert/api/prometheus/api_prometheus.go index 99cb0d95530..c835fb2acdd 100644 --- a/pkg/services/ngalert/api/prometheus/api_prometheus.go +++ b/pkg/services/ngalert/api/prometheus/api_prometheus.go @@ -477,6 +477,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt } ruleGroups := opts.Query["rule_group"] + ruleUIDs := opts.Query["rule_uid"] receiverName := opts.Query.Get("receiver_name") title := opts.Query.Get("search.rule_name") @@ -515,6 +516,7 @@ func PrepareRuleGroupStatusesV2(log log.Logger, store ListAlertRulesStoreV2, opt ListAlertRulesQuery: ngmodels.ListAlertRulesQuery{ OrgID: opts.OrgID, NamespaceUIDs: namespaceUIDs, + RuleUIDs: ruleUIDs, DashboardUID: dashboardUID, PanelID: panelID, RuleGroups: ruleGroups, @@ -650,6 +652,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru } ruleGroups := opts.Query["rule_group"] + ruleUIDs := opts.Query["rule_uid"] receiverName := opts.Query.Get("receiver_name") title := opts.Query.Get("search.rule_name") @@ -658,6 +661,7 @@ func PrepareRuleGroupStatuses(log log.Logger, store ListAlertRulesStore, opts Ru alertRuleQuery := ngmodels.ListAlertRulesQuery{ OrgID: opts.OrgID, NamespaceUIDs: namespaceUIDs, + RuleUIDs: ruleUIDs, DashboardUID: dashboardUID, PanelID: panelID, RuleGroups: ruleGroups, diff --git a/pkg/services/ngalert/api/prometheus_conversion.go b/pkg/services/ngalert/api/prometheus_conversion.go index 4006dcb0e10..588ea22bd11 100644 --- a/pkg/services/ngalert/api/prometheus_conversion.go +++ b/pkg/services/ngalert/api/prometheus_conversion.go @@ -5,7 +5,7 @@ import ( "io" "mime" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/errutil" diff --git a/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.hcl b/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.hcl index 2f71ea87efc..c8e64dc1a85 100644 --- a/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.hcl +++ b/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.hcl @@ -38,6 +38,8 @@ resource "grafana_contact_point" "contact_point_2b661702215368fe" { url = "[REDACTED]" title = "test-title" message = "test-message" + hide_open_button = true + hide_version_info = true } jira { diff --git a/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.json b/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.json index 1857db2807f..b8894dfe865 100644 --- a/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.json +++ b/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.json @@ -101,6 +101,8 @@ "uid": "googlechat-uid", "type": "googlechat", "settings": { + "hide_open_button": true, + "hide_version_info": true, "http_config": { "oauth2": { "client_id": "test-client-id", diff --git a/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.yaml b/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.yaml index 83e31ec0203..bc8f6c410d0 100644 --- a/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.yaml +++ b/pkg/services/ngalert/api/test-data/receiver-exports/redacted/all-integrations.yaml @@ -119,6 +119,8 @@ contactPoints: - uid: googlechat-uid type: googlechat settings: + hide_open_button: true + hide_version_info: true http_config: oauth2: client_id: test-client-id diff --git a/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.hcl b/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.hcl index aebe516faaa..ef5f1374b68 100644 --- a/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.hcl +++ b/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.hcl @@ -30,9 +30,11 @@ resource "grafana_contact_point" "contact_point_2b661702215368fe" { } googlechat { - url = "http://localhost" - title = "test-title" - message = "test-message" + url = "http://localhost" + title = "test-title" + message = "test-message" + hide_open_button = true + hide_version_info = true } jira { diff --git a/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.json b/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.json index 1532bdd8ac4..b75d624a85c 100644 --- a/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.json +++ b/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.json @@ -101,6 +101,8 @@ "uid": "googlechat-uid", "type": "googlechat", "settings": { + "hide_open_button": true, + "hide_version_info": true, "http_config": { "oauth2": { "client_id": "test-client-id", diff --git a/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.yaml b/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.yaml index a6af53fcb55..9cfc6e7c385 100644 --- a/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.yaml +++ b/pkg/services/ngalert/api/test-data/receiver-exports/unredacted/all-integrations.yaml @@ -119,6 +119,8 @@ contactPoints: - uid: googlechat-uid type: googlechat settings: + hide_open_button: true + hide_version_info: true http_config: oauth2: client_id: test-client-id diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index ecd19849ea4..4dad304af75 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -13,7 +13,7 @@ import ( "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/common/model" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/alerting/definition" alertingmodels "github.com/grafana/alerting/models" diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index 8c4b7efa509..87098ebbd30 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -14,7 +14,7 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) //go:embed test-data/*.* diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go index 46f64d096d1..c3fce7cb1b4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_validation.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/grafana/alerting/templates" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) func (t *NotificationTemplate) Validate() error { diff --git a/pkg/services/ngalert/api/tooling/definitions/contact_points.go b/pkg/services/ngalert/api/tooling/definitions/contact_points.go index eda7c545b06..9980736ede6 100644 --- a/pkg/services/ngalert/api/tooling/definitions/contact_points.go +++ b/pkg/services/ngalert/api/tooling/definitions/contact_points.go @@ -58,8 +58,10 @@ type GooglechatIntegration struct { URL Secret `json:"url" yaml:"url" hcl:"url"` - Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` - Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + Title *string `json:"title,omitempty" yaml:"title,omitempty" hcl:"title"` + Message *string `json:"message,omitempty" yaml:"message,omitempty" hcl:"message"` + HideOpenButton *bool `json:"hide_open_button,omitempty" yaml:"hide_open_button,omitempty" hcl:"hide_open_button"` + HideVersionInfo *bool `json:"hide_version_info,omitempty" yaml:"hide_version_info,omitempty" hcl:"hide_version_info"` } type JiraIntegration struct { diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index 21e6b438ac2..15cdb16d185 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -617,6 +617,9 @@ type GettableGrafanaRule struct { Metadata *AlertRuleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` GUID string `json:"guid" yaml:"guid"` MissingSeriesEvalsToResolve *int64 `json:"missing_series_evals_to_resolve,omitempty" yaml:"missing_series_evals_to_resolve,omitempty"` + + // Field is only populated when listing alert rule versions. + Message string `yaml:"message,omitempty" json:"message,omitempty"` } // UserInfo represents user-related information, including a unique identifier and a name. diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler_test.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler_test.go index 781d02dc8b1..019cdb99e23 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler_test.go @@ -6,7 +6,7 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) func Test_Rule_Marshaling(t *testing.T) { diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies_test.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies_test.go index 0176fbd1815..9c6469f1fcb 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) func TestNotificationPolicyExportMarshal(t *testing.T) { diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index e5840c992a7..c62f15ed7f4 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -13,7 +13,7 @@ import ( "strconv" "strings" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/accesscontrol" diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 9f899bc2af7..a4e1e8fcc04 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -369,6 +369,13 @@ type AlertRule struct { MissingSeriesEvalsToResolve *int64 } +type AlertRuleVersion struct { + AlertRule + + // Message is only stored in the alert_rule_version table. + Message string +} + type AlertRuleMetadata struct { EditorSettings EditorSettings `json:"editor_settings"` PrometheusStyleRule *PrometheusStyleRule `json:"prometheus_style_rule,omitempty"` diff --git a/pkg/services/ngalert/models/alert_rule_test.go b/pkg/services/ngalert/models/alert_rule_test.go index d42b5ae176f..f713cc19570 100644 --- a/pkg/services/ngalert/models/alert_rule_test.go +++ b/pkg/services/ngalert/models/alert_rule_test.go @@ -15,8 +15,8 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" "golang.org/x/exp/maps" - "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" diff --git a/pkg/services/ngalert/notifier/testreceivers.go b/pkg/services/ngalert/notifier/testreceivers.go index 41bd2a181c7..851521c61b5 100644 --- a/pkg/services/ngalert/notifier/testreceivers.go +++ b/pkg/services/ngalert/notifier/testreceivers.go @@ -44,7 +44,7 @@ func (am *alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei } AddDefaultLabelsAndAnnotations(a) return am.Base.TestReceivers(ctx, alertingNotify.TestReceiversConfigBodyParams{ - Alert: &alertingNotify.TestReceiversConfigAlertParams{ + Alert: &models.TestReceiversConfigAlertParams{ Annotations: v2.APILabelSetToModelLabelSet(a.Annotations), Labels: v2.APILabelSetToModelLabelSet(a.Labels), }, diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index 82318bba1e6..13c95e70aa4 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -6,7 +6,7 @@ import ( "time" "github.com/google/uuid" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/services/datasources" diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index ae4cf4a73e8..503cd76dd64 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" prommodel "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/expr/mathexp" diff --git a/pkg/services/ngalert/prom/models_test.go b/pkg/services/ngalert/prom/models_test.go index f2184a4fffb..d10b828e936 100644 --- a/pkg/services/ngalert/prom/models_test.go +++ b/pkg/services/ngalert/prom/models_test.go @@ -6,7 +6,7 @@ import ( prommodel "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 81175e14766..a7c99f8518c 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -30,7 +30,7 @@ import ( "github.com/prometheus/alertmanager/pkg/labels" "github.com/prometheus/client_golang/prometheus" common_config "github.com/prometheus/common/config" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" @@ -625,9 +625,9 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei } apiReceivers := alertingNotify.PostableAPIReceiversToAPIReceivers(decryptedReceivers) - var alert *alertingNotify.TestReceiversConfigAlertParams + var alert *alertingModels.TestReceiversConfigAlertParams if c.Alert != nil { - alert = &alertingNotify.TestReceiversConfigAlertParams{Annotations: c.Alert.Annotations, Labels: c.Alert.Labels} + alert = &alertingModels.TestReceiversConfigAlertParams{Annotations: c.Alert.Annotations, Labels: c.Alert.Labels} } return am.mimirClient.TestReceivers(ctx, alertingNotify.TestReceiversConfigBodyParams{ diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index b183943d2ac..953ef3ef2d6 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -27,12 +27,12 @@ import ( "github.com/prometheus/client_golang/prometheus" common_config "github.com/prometheus/common/config" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" alertingClusterPB "github.com/grafana/alerting/cluster/clusterpb" "github.com/grafana/alerting/definition" alertingModels "github.com/grafana/alerting/models" "github.com/grafana/alerting/notify" - "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" diff --git a/pkg/services/ngalert/sender/notifier.go b/pkg/services/ngalert/sender/notifier.go index 2ba819a8c41..061ca7d138c 100644 --- a/pkg/services/ngalert/sender/notifier.go +++ b/pkg/services/ngalert/sender/notifier.go @@ -38,7 +38,7 @@ import ( "github.com/prometheus/common/promslog" "github.com/prometheus/common/version" "github.com/prometheus/sigv4" - "gopkg.in/yaml.v2" + "go.yaml.in/yaml/v3" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/discovery/targetgroup" diff --git a/pkg/services/ngalert/sender/notifier_test.go b/pkg/services/ngalert/sender/notifier_test.go index 20caf7e9aea..e7e9aedec96 100644 --- a/pkg/services/ngalert/sender/notifier_test.go +++ b/pkg/services/ngalert/sender/notifier_test.go @@ -38,7 +38,7 @@ import ( "github.com/prometheus/common/promslog" "github.com/stretchr/testify/require" "go.uber.org/atomic" - "gopkg.in/yaml.v2" + "go.yaml.in/yaml/v2" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/discovery" diff --git a/pkg/services/ngalert/state/cache.go b/pkg/services/ngalert/state/cache.go index 592fb292697..c1598d4aa3b 100644 --- a/pkg/services/ngalert/state/cache.go +++ b/pkg/services/ngalert/state/cache.go @@ -25,6 +25,13 @@ type ruleStates struct { type cache struct { states map[int64]map[string]*ruleStates // orgID > alertRuleUID > stateID > state mtxStates sync.RWMutex + metrics alertMetrics +} + +type alertMetrics struct { + lastUpdate time.Time + mtx sync.RWMutex + stateCounts map[eval.State]float64 } func newCache() *cache { @@ -33,6 +40,51 @@ func newCache() *cache { } } +func (c *cache) calcMetrics(states map[eval.State]struct{}) map[eval.State]float64 { + c.mtxStates.RLock() + defer c.mtxStates.RUnlock() + counts := make(map[eval.State]float64, len(states)) + for state := range states { + counts[state] = 0 + } + for _, orgMap := range c.states { + for _, rule := range orgMap { + for _, st := range rule.states { + for state := range states { + if st.State == state { + counts[state] += 1 + } + } + } + } + } + return counts +} + +func (c *cache) updateMetrics() { + c.metrics.mtx.Lock() + defer c.metrics.mtx.Unlock() + if time.Since(c.metrics.lastUpdate) < time.Second { + return // avoid updating too frequently + } + newMetrics := c.calcMetrics(map[eval.State]struct{}{ + eval.Normal: {}, + eval.Alerting: {}, + eval.Pending: {}, + eval.Error: {}, + eval.NoData: {}, + eval.Recovering: {}, + }) + c.metrics.lastUpdate = time.Now() + c.metrics.stateCounts = newMetrics +} + +func (c *cache) countAlertsBy(state eval.State) float64 { + c.metrics.mtx.RLock() + defer c.metrics.mtx.RUnlock() + return c.metrics.stateCounts[state] +} + // RegisterMetrics registers a set of Gauges in the form of collectors for the alerts in the cache. func (c *cache) RegisterMetrics(r prometheus.Registerer) { newAlertCountByState := func(state eval.State) prometheus.GaugeFunc { @@ -43,6 +95,7 @@ func (c *cache) RegisterMetrics(r prometheus.Registerer) { Help: "How many alerts by state are in the scheduler.", ConstLabels: prometheus.Labels{"state": strings.ToLower(state.String())}, }, func() float64 { + c.updateMetrics() return c.countAlertsBy(state) }) } @@ -55,23 +108,6 @@ func (c *cache) RegisterMetrics(r prometheus.Registerer) { r.MustRegister(newAlertCountByState(eval.Recovering)) } -func (c *cache) countAlertsBy(state eval.State) float64 { - c.mtxStates.RLock() - defer c.mtxStates.RUnlock() - var count float64 - for _, orgMap := range c.states { - for _, rule := range orgMap { - for _, st := range rule.states { - if st.State == state { - count++ - } - } - } - } - - return count -} - func expandAnnotationsAndLabels(ctx context.Context, log log.Logger, alertRule *ngModels.AlertRule, result eval.Result, extraLabels data.Labels, externalURL *url.URL) (data.Labels, data.Labels) { var reserved []string resultLabels := result.Instance diff --git a/pkg/services/ngalert/state/cache_test.go b/pkg/services/ngalert/state/cache_test.go index f83c1aa9698..985e7fc1f05 100644 --- a/pkg/services/ngalert/state/cache_test.go +++ b/pkg/services/ngalert/state/cache_test.go @@ -216,6 +216,115 @@ func TestCacheMetrics(t *testing.T) { }) } +func TestCacheMetricsDebounce(t *testing.T) { + t.Run("updates after debounce window", func(t *testing.T) { + orgID := int64(1) + + reg := prometheus.NewPedanticRegistry() + cache := newCache() + + // Add initial state: 1 alerting + cache.set(&State{ + OrgID: orgID, + AlertRuleUID: "rule1", + CacheID: data.Fingerprint(rand.Int63()), + State: eval.Alerting, + }) + + cache.RegisterMetrics(reg) + + // First gather should reflect the single alerting instance + expectedInitial := ` + # HELP grafana_alerting_alerts How many alerts by state are in the scheduler. + # TYPE grafana_alerting_alerts gauge + grafana_alerting_alerts{state="alerting"} 1 + grafana_alerting_alerts{state="error"} 0 + grafana_alerting_alerts{state="nodata"} 0 + grafana_alerting_alerts{state="normal"} 0 + grafana_alerting_alerts{state="pending"} 0 + grafana_alerting_alerts{state="recovering"} 0 + ` + + err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedInitial), "grafana_alerting_alerts") + require.NoError(t, err) + + // Modify cache immediately: add 2 more alerting and 1 error state. + // Due to debounce (1s), next gather should still return the initial values. + cache.set(&State{OrgID: orgID, AlertRuleUID: "rule1", CacheID: data.Fingerprint(rand.Int63()), State: eval.Alerting}) + cache.set(&State{OrgID: orgID, AlertRuleUID: "rule1", CacheID: data.Fingerprint(rand.Int63()), State: eval.Alerting}) + cache.set(&State{OrgID: orgID, AlertRuleUID: "rule1", CacheID: data.Fingerprint(rand.Int63()), State: eval.Error}) + + // Immediate gather should still show the initial counts because of debounce. + err = testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedInitial), "grafana_alerting_alerts") + require.NoError(t, err) + + // Bypass debounce by setting lastUpdate in the past (>1s) under lock. + cache.metrics.mtx.Lock() + cache.metrics.lastUpdate = time.Now().Add(-2 * time.Second) + cache.metrics.mtx.Unlock() + + expectedAfter := ` + # HELP grafana_alerting_alerts How many alerts by state are in the scheduler. + # TYPE grafana_alerting_alerts gauge + grafana_alerting_alerts{state="alerting"} 3 + grafana_alerting_alerts{state="error"} 1 + grafana_alerting_alerts{state="nodata"} 0 + grafana_alerting_alerts{state="normal"} 0 + grafana_alerting_alerts{state="pending"} 0 + grafana_alerting_alerts{state="recovering"} 0 + ` + + err = testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedAfter), "grafana_alerting_alerts") + require.NoError(t, err) + }) + + t.Run("no update within debounce window", func(t *testing.T) { + orgID := int64(1) + + reg := prometheus.NewPedanticRegistry() + cache := newCache() + + // Seed with one alerting state + cache.set(&State{OrgID: orgID, AlertRuleUID: "rule1", CacheID: data.Fingerprint(rand.Int63()), State: eval.Alerting}) + + cache.RegisterMetrics(reg) + + // Initial gather populates metrics (1 alerting) + expectedInitial := ` + # HELP grafana_alerting_alerts How many alerts by state are in the scheduler. + # TYPE grafana_alerting_alerts gauge + grafana_alerting_alerts{state="alerting"} 1 + grafana_alerting_alerts{state="error"} 0 + grafana_alerting_alerts{state="nodata"} 0 + grafana_alerting_alerts{state="normal"} 0 + grafana_alerting_alerts{state="pending"} 0 + grafana_alerting_alerts{state="recovering"} 0 + ` + err := testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedInitial), "grafana_alerting_alerts") + require.NoError(t, err) + + // Add more states that would change counts if recalculated + cache.set(&State{OrgID: orgID, AlertRuleUID: "rule1", CacheID: data.Fingerprint(rand.Int63()), State: eval.Alerting}) + cache.set(&State{OrgID: orgID, AlertRuleUID: "rule1", CacheID: data.Fingerprint(rand.Int63()), State: eval.Error}) + + // Force debounce window by setting lastUpdate to now, capture it + cache.metrics.mtx.Lock() + ts := time.Now() + cache.metrics.lastUpdate = ts + cache.metrics.mtx.Unlock() + + // Gather should NOT update metrics due to debounce; counts should remain initial + err = testutil.GatherAndCompare(reg, bytes.NewBufferString(expectedInitial), "grafana_alerting_alerts") + require.NoError(t, err) + + // Confirm lastUpdate did not change (no metrics refresh happened) + cache.metrics.mtx.RLock() + tsAfter := cache.metrics.lastUpdate + cache.metrics.mtx.RUnlock() + require.True(t, tsAfter.Equal(ts), "expected lastUpdate to remain unchanged within debounce window") + }) +} + func randomSate(ruleKey models.AlertRuleKey) State { return State{ OrgID: ruleKey.OrgID, diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 9044bb369c5..b05ba01a562 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -192,8 +192,8 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler return result, err } -func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRule, error) { - alertRules := make([]*ngmodels.AlertRule, 0) +func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid string) ([]*ngmodels.AlertRuleVersion, error) { + alertRules := make([]*ngmodels.AlertRuleVersion, 0) err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error { rows, err := sess.Table(new(alertRuleVersion)).Where("rule_org_id = ? AND rule_guid = ?", orgID, guid).Asc("id").Rows(new(alertRuleVersion)) if err != nil { @@ -213,7 +213,7 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid st if previousVersion != nil && previousVersion.EqualSpec(*rule) { continue } - converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger) + converted, err := alertRuleVersionToModelsAlertRuleVersion(*rule, st.Logger) if err != nil { st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID) continue @@ -226,7 +226,7 @@ func (st DBstore) GetAlertRuleVersions(ctx context.Context, orgID int64, guid st if err != nil { return nil, err } - slices.SortFunc(alertRules, func(a, b *ngmodels.AlertRule) int { + slices.SortFunc(alertRules, func(a, b *ngmodels.AlertRuleVersion) int { if a.ID > b.ID { return -1 } @@ -257,6 +257,7 @@ func (st DBstore) ListDeletedRules(ctx context.Context, orgID int64) ([]*ngmodel st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "GetAlertRuleVersions", "error", err) continue } + // Note: Message is not returned as a message cannot be set when deleting rules. converted, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(*rule), st.Logger) if err != nil { st.Logger.Error("Invalid rule found in DB store, cannot convert, ignoring it", "func", "GetAlertRuleVersions", "error", err, "version_id", rule.ID) diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 0c49528f8d6..9a491120c8c 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -1682,7 +1682,7 @@ func TestIntegrationGetRuleVersions(t *testing.T) { require.NoError(t, err) assert.Len(t, versions, 2) assert.IsDecreasing(t, versions[0].ID, versions[1].ID) - diff := versions[1].Diff(versions[0], AlertRuleFieldsToIgnoreInDiff[:]...) + diff := versions[1].Diff(&versions[0].AlertRule, AlertRuleFieldsToIgnoreInDiff[:]...) assert.ElementsMatch(t, []string{"Title", "RuleGroupIndex"}, diff.Paths()) }) @@ -1712,7 +1712,7 @@ func TestIntegrationGetRuleVersions(t *testing.T) { versions, err := store.GetAlertRuleVersions(context.Background(), ruleV3.OrgID, ruleV3.GUID) require.NoError(t, err) assert.Len(t, versions, 3) - diff := versions[0].Diff(versions[1], AlertRuleFieldsToIgnoreInDiff[:]...) + diff := versions[0].Diff(&versions[1].AlertRule, AlertRuleFieldsToIgnoreInDiff[:]...) assert.ElementsMatch(t, []string{"RuleGroup", "NamespaceUID"}, diff.Paths()) }) } diff --git a/pkg/services/ngalert/store/compat.go b/pkg/services/ngalert/store/compat.go index 709450d5ed8..4f4194facc1 100644 --- a/pkg/services/ngalert/store/compat.go +++ b/pkg/services/ngalert/store/compat.go @@ -210,6 +210,7 @@ func alertRuleToAlertRuleVersion(rule alertRule) alertRuleVersion { Version: rule.Version, Created: rule.Updated, // assuming the Updated time as the creation time CreatedBy: rule.UpdatedBy, + Message: "", // Message is set by caller when creating versions Title: rule.Title, Condition: rule.Condition, Data: rule.Data, @@ -261,3 +262,15 @@ func alertRuleVersionToAlertRule(version alertRuleVersion) alertRule { MissingSeriesEvalsToResolve: version.MissingSeriesEvalsToResolve, } } + +func alertRuleVersionToModelsAlertRuleVersion(version alertRuleVersion, l log.Logger) (models.AlertRuleVersion, error) { + result, err := alertRuleToModelsAlertRule(alertRuleVersionToAlertRule(version), l) + if err != nil { + return models.AlertRuleVersion{}, err + } + + return models.AlertRuleVersion{ + AlertRule: result, + Message: version.Message, + }, nil +} diff --git a/pkg/services/ngalert/store/models.go b/pkg/services/ngalert/store/models.go index 254ec8cb048..24167a225dd 100644 --- a/pkg/services/ngalert/store/models.go +++ b/pkg/services/ngalert/store/models.go @@ -69,10 +69,11 @@ type alertRuleVersion struct { NotificationSettings string `xorm:"notification_settings"` Metadata string `xorm:"metadata"` MissingSeriesEvalsToResolve *int64 `xorm:"missing_series_evals_to_resolve"` + Message string } // EqualSpec compares two alertRuleVersion objects for equality based on their specifications and returns true if they match. -// The comparison is very basic and can produce false-negative. Fields excluded: ID, ParentVersion, RestoredFrom, Version, Created, RuleGroupIndex and CreatedBy +// The comparison is very basic and can produce false-negative. Fields excluded: ID, ParentVersion, RestoredFrom, Version, Created, RuleGroupIndex, CreatedBy and Message func (a alertRuleVersion) EqualSpec(b alertRuleVersion) bool { return a.RuleOrgID == b.RuleOrgID && a.RuleGUID == b.RuleGUID && diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index a0dd60e32e5..09abeaffba3 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -23,7 +23,7 @@ type RuleStore struct { mtx sync.Mutex // OrgID -> RuleGroup -> Namespace -> Rules Rules map[int64][]*models.AlertRule - History map[string][]*models.AlertRule + History map[string][]*models.AlertRuleVersion Deleted map[int64][]*models.AlertRule Hook func(cmd any) error // use Hook if you need to intercept some query and return an error RecordedOps []any @@ -43,7 +43,7 @@ func NewRuleStore(t *testing.T) *RuleStore { return nil }, Folders: map[int64][]*folder.Folder{}, - History: map[string][]*models.AlertRule{}, + History: map[string][]*models.AlertRuleVersion{}, } } @@ -55,7 +55,10 @@ mainloop: for _, r := range rules { rgs := f.Rules[r.OrgID] cp := models.CopyRule(r) - f.History[r.GUID] = append(f.History[r.GUID], cp) + f.History[r.GUID] = append(f.History[r.GUID], &models.AlertRuleVersion{ + AlertRule: *cp, + Message: "", + }) for idx, rulePtr := range rgs { if rulePtr.UID == r.UID { rgs[idx] = r @@ -87,6 +90,18 @@ mainloop: } } +// AppendHistory appends to rules to the version history with the given change message. +func (f *RuleStore) AppendHistory(guid string, rules []*models.AlertRule, message string) { + versions := make([]*models.AlertRuleVersion, len(rules)) + for i := range rules { + versions[i] = &models.AlertRuleVersion{ + AlertRule: *rules[i], + Message: message, + } + } + f.History[guid] = append(f.History[guid], versions...) +} + // GetRecordedCommands filters recorded commands using predicate function. Returns the subset of the recorded commands that meet the predicate func (f *RuleStore) GetRecordedCommands(predicate func(cmd any) (any, bool)) []any { f.mtx.Lock() @@ -563,7 +578,7 @@ func (f *RuleStore) GetNamespacesByRuleUID(ctx context.Context, orgID int64, uid return namespacesMap, nil } -func (f *RuleStore) GetAlertRuleVersions(_ context.Context, orgID int64, guid string) ([]*models.AlertRule, error) { +func (f *RuleStore) GetAlertRuleVersions(_ context.Context, orgID int64, guid string) ([]*models.AlertRuleVersion, error) { f.mtx.Lock() defer f.mtx.Unlock() diff --git a/pkg/services/oauthtoken/oauth_token.go b/pkg/services/oauthtoken/oauth_token.go index ee0de1007b2..0efe5e553f3 100644 --- a/pkg/services/oauthtoken/oauth_token.go +++ b/pkg/services/oauthtoken/oauth_token.go @@ -473,6 +473,10 @@ func (o *Service) tryGetOrRefreshOAuthToken(ctx context.Context, persistedToken ) } + if token.RefreshToken == "" { + ctxLogger.Warn("Refresh token is missing after token refresh", "authmodule", tokenRefreshMetadata.AuthModule) + } + //nolint:staticcheck // not yet migrated to OpenFeature if !o.features.IsEnabledGlobally(featuremgmt.FlagImprovedExternalSessionHandling) { updateAuthCommand := &login.UpdateAuthInfoCommand{ diff --git a/pkg/services/provisioning/alerting/config_reader.go b/pkg/services/provisioning/alerting/config_reader.go index b6ca9d2c3dd..8073ff64348 100644 --- a/pkg/services/provisioning/alerting/config_reader.go +++ b/pkg/services/provisioning/alerting/config_reader.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/infra/log" ) diff --git a/pkg/services/provisioning/alerting/contact_point_types_test.go b/pkg/services/provisioning/alerting/contact_point_types_test.go index 57ef9acfca0..28af33c42ae 100644 --- a/pkg/services/provisioning/alerting/contact_point_types_test.go +++ b/pkg/services/provisioning/alerting/contact_point_types_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/services/provisioning/values" ) diff --git a/pkg/services/provisioning/alerting/notification_policy_types_test.go b/pkg/services/provisioning/alerting/notification_policy_types_test.go index 5ec98c8722f..72a60cb5155 100644 --- a/pkg/services/provisioning/alerting/notification_policy_types_test.go +++ b/pkg/services/provisioning/alerting/notification_policy_types_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) func TestNotificationPolicy(t *testing.T) { diff --git a/pkg/services/provisioning/alerting/rules_types_test.go b/pkg/services/provisioning/alerting/rules_types_test.go index 9c4e819ff73..c2cff56e419 100644 --- a/pkg/services/provisioning/alerting/rules_types_test.go +++ b/pkg/services/provisioning/alerting/rules_types_test.go @@ -6,7 +6,7 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/provisioning/values" diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 3cf08eb01c2..509665135eb 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/provisioning/utils" diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 1d5a992d1f7..cea2eb36207 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/datasources" diff --git a/pkg/services/provisioning/plugins/config_reader.go b/pkg/services/provisioning/plugins/config_reader.go index 52684de6326..36790a6e58c 100644 --- a/pkg/services/provisioning/plugins/config_reader.go +++ b/pkg/services/provisioning/plugins/config_reader.go @@ -9,7 +9,7 @@ import ( "path/filepath" "strings" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" diff --git a/pkg/services/provisioning/values/values_test.go b/pkg/services/provisioning/values/values_test.go index 93776cb7a60..61378f124d7 100644 --- a/pkg/services/provisioning/values/values_test.go +++ b/pkg/services/provisioning/values/values_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" "gopkg.in/ini.v1" - "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index 138812ebd03..34abc9a2dbf 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -333,12 +333,9 @@ func (s *ServiceImpl) handleQuerySingleDatasource(ctx context.Context, user iden } func getTimeRange(query *simplejson.Json, globalFrom string, globalTo string) (string, string, error) { - tr, ok := query.CheckGet("_timeRange") - if !ok { // timeRange json node does not exist - tr, ok = query.CheckGet("timeRange") // try the old name for backward compatibility - if !ok { // timeRange json node does not exist, use global from/to - return globalFrom, globalTo, nil - } + tr, ok := query.CheckGet("timeRange") + if !ok { // timeRange json node does not exist, use global from/to + return globalFrom, globalTo, nil } from, err := tr.Get("from").String() if err != nil { @@ -352,6 +349,40 @@ func getTimeRange(query *simplejson.Json, globalFrom string, globalTo string) (s return from, to, nil } +const timeRangeKey = "timeRange" + +func queryToJson(query *simplejson.Json, supportLocalTimeRange bool) ([]byte, error) { + if !supportLocalTimeRange { + return query.MarshalJSON() + } + + // we need to remove the `timeRange` attribute from the JSON, if it exists there, + // because it might cause complications with certain data sources. + _, has := query.CheckGet(timeRangeKey) + if !has { + return query.MarshalJSON() + } + + qMap, err := query.Map() + if err != nil { + return nil, err + } + + // we do not want to modify the query, + // so we create a copy without + // the timeRange attribute + d := make(map[string]any) + + // without the `timeRange` attribute + for k, v := range qMap { + if k != timeRangeKey { + d[k] = v + } + } + + return simplejson.NewFromAny(d).MarshalJSON() +} + // parseRequest parses a request into parsed queries grouped by datasource uid func (s *ServiceImpl) parseMetricRequest(ctx context.Context, user identity.Requester, skipDSCache bool, reqDTO dtos.MetricRequest, supportLocalTimeRange bool) (*parsedRequest, error) { if len(reqDTO.Queries) == 0 { @@ -397,7 +428,7 @@ func (s *ServiceImpl) parseMetricRequest(ctx context.Context, user identity.Requ timeRange = gtime.NewTimeRange(reqDTO.From, reqDTO.To) } - modelJSON, err := query.MarshalJSON() + modelJSON, err := queryToJson(query, supportLocalTimeRange) if err != nil { return nil, err } diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index 02a4dea4824..7763bdcaecf 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -334,62 +334,6 @@ func TestIntegrationParseMetricRequest(t *testing.T) { assert.Equal(t, int64(1753944629000), q.query.TimeRange.To.UnixMilli()) } }) - t.Run("Test a datasource query with local time range, old attribute-name", func(t *testing.T) { - tc := setup(t, false, nil) - mr := metricRequestWithQueries(t, `{ - "refId": "A", - "datasource": { - "uid": "gIEkMvIVz", - "type": "postgres" - }, - "timeRange": { - "from": "1753944618000", - "to": "1753944619000" - } - }`, `{ - "refId": "B", - "datasource": { - "uid": "gIEkMvIVz", - "type": "postgres" - }, - "timeRange": { - "from": "1753944628000", - "to": "1753944629000" - } - }`) - mr.From = "" - mr.To = "" - - verifyTimestamps := func(parsedReq *parsedRequest, ts1 int64, ts2 int64, ts3 int64, ts4 int64) { - require.NotNil(t, parsedReq) - assert.Len(t, parsedReq.parsedQueries, 1) - assert.Contains(t, parsedReq.parsedQueries, "gIEkMvIVz") - queries := parsedReq.getFlattenedQueries() - assert.Len(t, queries, 2) - - assert.Equal(t, ts1, queries[0].query.TimeRange.From.UnixMilli()) - assert.Equal(t, ts2, queries[0].query.TimeRange.To.UnixMilli()) - - assert.Equal(t, ts3, queries[1].query.TimeRange.From.UnixMilli()) - assert.Equal(t, ts4, queries[1].query.TimeRange.To.UnixMilli()) - } - - // with flag enabled - parsedReq, err := tc.queryService.parseMetricRequest(context.Background(), tc.signedInUser, true, mr, true) - require.NoError(t, err) - - verifyTimestamps(parsedReq, - int64(1753944618000), - int64(1753944619000), - int64(1753944628000), - int64(1753944629000)) - - // with flag disabled - parsedReq2, err := tc.queryService.parseMetricRequest(context.Background(), tc.signedInUser, true, mr, false) - require.NoError(t, err) - - verifyTimestamps(parsedReq2, int64(0), int64(0), int64(0), int64(0)) - }) t.Run("Test a datasource query with local time range", func(t *testing.T) { tc := setup(t, false, nil) mr := metricRequestWithQueries(t, `{ @@ -408,43 +352,86 @@ func TestIntegrationParseMetricRequest(t *testing.T) { "uid": "gIEkMvIVz", "type": "postgres" }, - "_timeRange": { + "timeRange": { "from": "1753944628000", "to": "1753944629000" } + }`, `{ + "refId": "C", + "datasource": { + "uid": "gIEkMvIVz", + "type": "postgres" + } }`) - mr.From = "" - mr.To = "" + mr.From = "1753944638000" + mr.To = "1753944639000" - verifyTimestamps := func(parsedReq *parsedRequest, ts1 int64, ts2 int64, ts3 int64, ts4 int64) { + q0Copy := mr.Queries[0].DeepCopy() + q1Copy := mr.Queries[1].DeepCopy() + + verifyTimestamps := func( + parsedReq *parsedRequest, + ts1 int64, ts2 int64, q0JSON string, + ts3 int64, ts4 int64, q1JSON string, + ts5 int64, ts6 int64, q2JSON string, + ) { require.NotNil(t, parsedReq) assert.Len(t, parsedReq.parsedQueries, 1) assert.Contains(t, parsedReq.parsedQueries, "gIEkMvIVz") queries := parsedReq.getFlattenedQueries() - assert.Len(t, queries, 2) + assert.Len(t, queries, 3) + assert.Equal(t, "A", queries[0].query.RefID) assert.Equal(t, ts1, queries[0].query.TimeRange.From.UnixMilli()) assert.Equal(t, ts2, queries[0].query.TimeRange.To.UnixMilli()) + assert.JSONEq(t, q0JSON, string(queries[0].query.JSON)) + assert.Equal(t, "B", queries[1].query.RefID) assert.Equal(t, ts3, queries[1].query.TimeRange.From.UnixMilli()) assert.Equal(t, ts4, queries[1].query.TimeRange.To.UnixMilli()) + assert.JSONEq(t, q1JSON, string(queries[1].query.JSON)) + + assert.Equal(t, "C", queries[2].query.RefID) + assert.Equal(t, ts5, queries[2].query.TimeRange.From.UnixMilli()) + assert.Equal(t, ts6, queries[2].query.TimeRange.To.UnixMilli()) + assert.JSONEq(t, q2JSON, string(queries[2].query.JSON)) } // with flag enabled parsedReq, err := tc.queryService.parseMetricRequest(context.Background(), tc.signedInUser, true, mr, true) require.NoError(t, err) + // verify the query-structures were not mutated + require.Equal(t, q0Copy, mr.Queries[0]) + require.Equal(t, q1Copy, mr.Queries[1]) + verifyTimestamps(parsedReq, int64(1753944618000), int64(1753944619000), + `{"datasource":{"type":"postgres","uid":"gIEkMvIVz"},"refId":"A"}`, int64(1753944628000), - int64(1753944629000)) + int64(1753944629000), + `{"datasource":{"type":"postgres","uid":"gIEkMvIVz"},"refId":"B"}`, + int64(1753944638000), + int64(1753944639000), + `{"datasource":{"type":"postgres","uid":"gIEkMvIVz"},"refId":"C"}`, + ) // with flag disabled parsedReq2, err := tc.queryService.parseMetricRequest(context.Background(), tc.signedInUser, true, mr, false) require.NoError(t, err) - verifyTimestamps(parsedReq2, int64(0), int64(0), int64(0), int64(0)) + verifyTimestamps(parsedReq2, + int64(1753944638000), + int64(1753944639000), + `{"datasource":{"type":"postgres","uid":"gIEkMvIVz"},"refId":"A", "timeRange":{"from":"1753944618000", "to":"1753944619000"}}`, + int64(1753944638000), + int64(1753944639000), + `{"datasource":{"type":"postgres","uid":"gIEkMvIVz"},"refId":"B", "timeRange":{"from":"1753944628000", "to":"1753944629000"}}`, + int64(1753944638000), + int64(1753944639000), + `{"datasource":{"type":"postgres","uid":"gIEkMvIVz"},"refId":"C"}`, + ) }) t.Run("Test a datasource query with local time range, malformed to-value", func(t *testing.T) { @@ -455,7 +442,7 @@ func TestIntegrationParseMetricRequest(t *testing.T) { "uid": "gIEkMvIVz", "type": "postgres" }, - "_timeRange": { + "timeRange": { "from": "1753944618000", "to": 1753944619000 } @@ -474,7 +461,7 @@ func TestIntegrationParseMetricRequest(t *testing.T) { "uid": "gIEkMvIVz", "type": "postgres" }, - "_timeRange": { + "timeRange": { "from": "1753944618000" } }`) @@ -492,7 +479,7 @@ func TestIntegrationParseMetricRequest(t *testing.T) { "uid": "gIEkMvIVz", "type": "postgres" }, - "_timeRange": { + "timeRange": { "from": 1753944618000, "to": "1753944619000" } @@ -511,7 +498,7 @@ func TestIntegrationParseMetricRequest(t *testing.T) { "uid": "gIEkMvIVz", "type": "postgres" }, - "_timeRange": { + "timeRange": { "to": "1753944619000" } }`) @@ -774,6 +761,69 @@ func TestIntegrationQueryDataWithQSDSClient(t *testing.T) { }) } +func TestQueryToJson(t *testing.T) { + tests := []struct { + name string + data []byte + expected []byte + expectedError bool + }{ + { + name: "simple", + data: []byte(`{"refId":"A", "expr": "42", "timeRange":{"from":"111","to":"112"}}`), + expected: []byte(`{"refId":"A", "expr": "42"}`), + expectedError: false, + }, + { + name: "no time range", + data: []byte(`{"refId":"A", "expr": "42"}`), + expected: []byte(`{"refId":"A", "expr": "42"}`), + expectedError: false, + }, + { + name: "nested structures, numbers", + data: []byte(`{"a":{"b":["c","d"]},"intervalMs":60000, "timeRange":{"from":"111","to":"112"}}`), + expected: []byte(`{"a":{"b":["c","d"]},"intervalMs":60000}`), + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q, err := simplejson.NewJson(tt.data) + require.NoError(t, err) + + // first we go with supportLocalTimeRange=false + res, err := queryToJson(q, false) + require.NoError(t, err) + + // verify that we did not mutate the input + data2, err := q.MarshalJSON() + require.NoError(t, err) + require.JSONEq(t, string(data2), string(tt.data)) + + // verify the output is the same as the input (because we used supportLocalTimeRange=false) + require.JSONEq(t, string(res), string(tt.data)) + + // now we go with supportLocalTimeRange=true + + res, err = queryToJson(q, true) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + + require.JSONEq(t, string(tt.expected), string(res)) + } + + // verify that we did not mutate the input + data2, err = q.MarshalJSON() + require.NoError(t, err) + require.JSONEq(t, string(data2), string(tt.data)) + }) + } +} + func setup(t *testing.T, isMultiTenant bool, mockClient clientapi.QueryDataClient) *testContext { dss := []*datasources.DataSource{ {UID: "gIEkMvIVz", Type: "postgres"}, diff --git a/pkg/services/queryhistory/api.go b/pkg/services/queryhistory/api.go index 0d26718170e..797f0aaabc9 100644 --- a/pkg/services/queryhistory/api.go +++ b/pkg/services/queryhistory/api.go @@ -168,16 +168,6 @@ func (s *QueryHistoryService) starHandler(c *contextmodel.ReqContext) response.R if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) } - if s.k8sClients != nil { - if err := s.k8sClients.AddStar(c, queryUID); err != nil { - return response.Error(http.StatusInternalServerError, "Failed to star query in query history", err) - } - return response.JSON(http.StatusOK, QueryHistoryResponse{ - Result: QueryHistoryDTO{ - UID: queryUID, - Starred: true, - }}) - } query, err := s.StarQueryInQueryHistory(c.Req.Context(), c.SignedInUser, queryUID) if err != nil { @@ -202,17 +192,6 @@ func (s *QueryHistoryService) unstarHandler(c *contextmodel.ReqContext) response if len(queryUID) > 0 && !util.IsValidShortUID(queryUID) { return response.Error(http.StatusNotFound, "Query in query history not found", nil) } - if s.k8sClients != nil { - if err := s.k8sClients.RemoveStar(c, queryUID); err != nil { - return response.Error(http.StatusInternalServerError, "Failed to star query in query history", err) - } - return response.JSON(http.StatusOK, QueryHistoryResponse{ - Result: QueryHistoryDTO{ - UID: queryUID, - Starred: true, - }}) - } - query, err := s.UnstarQueryInQueryHistory(c.Req.Context(), c.SignedInUser, queryUID) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to unstar query in query history", err) diff --git a/pkg/services/queryhistory/client.go b/pkg/services/queryhistory/client.go deleted file mode 100644 index a134ef9e186..00000000000 --- a/pkg/services/queryhistory/client.go +++ /dev/null @@ -1,103 +0,0 @@ -package queryhistory - -import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - - authlib "github.com/grafana/authlib/types" - preferencesV1 "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" - "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/services/apiserver" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" -) - -type k8sClients struct { - namespacer authlib.NamespaceFormatter - configProvider apiserver.DirectRestConfigProvider -} - -// GetStars implements K8sClients. -func (k *k8sClients) GetStars(c *contextmodel.ReqContext) ([]string, error) { - dyn, err := dynamic.NewForConfig(k.configProvider.GetDirectRestConfig(c)) - if err != nil { - return nil, err - } - client := dyn.Resource(preferencesV1.StarsResourceInfo.GroupVersionResource()).Namespace(k.namespacer(c.OrgID)) - - ctx := c.Req.Context() - user, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - - obj, _ := client.Get(ctx, "user-"+user.GetIdentifier(), v1.GetOptions{}) - if obj != nil { - resources, ok, _ := unstructured.NestedSlice(obj.Object, "spec", "resource") - if ok && resources != nil { - for _, r := range resources { - tmp, ok := r.(map[string]any) - if ok { - g, _, _ := unstructured.NestedString(tmp, "group") - k, _, _ := unstructured.NestedString(tmp, "kind") - if k == "Query" && g == "history.grafana.app" { - names, _, _ := unstructured.NestedStringSlice(tmp, "names") - return names, nil - } - } - } - } - } - return []string{}, nil -} - -// AddStar implements K8sClients. -func (k *k8sClients) AddStar(c *contextmodel.ReqContext, uid string) error { - dyn, err := kubernetes.NewForConfig(k.configProvider.GetDirectRestConfig(c)) - if err != nil { - return err - } - - ctx := c.Req.Context() - user, err := identity.GetRequester(ctx) - if err != nil { - return err - } - - ns := k.namespacer(c.OrgID) - - client := dyn.RESTClient() - rsp := client.Put().AbsPath( - "apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns, - "stars", "user-"+user.GetIdentifier(), - "update", "history.grafana.app", "Query", uid, - ).Do(ctx) - - return rsp.Error() -} - -// RemoveStar implements K8sClients. -func (k *k8sClients) RemoveStar(c *contextmodel.ReqContext, uid string) error { - dyn, err := kubernetes.NewForConfig(k.configProvider.GetDirectRestConfig(c)) - if err != nil { - return err - } - - ctx := c.Req.Context() - user, err := identity.GetRequester(ctx) - if err != nil { - return err - } - - ns := k.namespacer(c.OrgID) - - client := dyn.RESTClient() - rsp := client.Delete().AbsPath( - "apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns, - "stars", "user-"+user.GetIdentifier(), - "update", "history.grafana.app", "Query", uid, - ).Do(ctx) - - return rsp.Error() -} diff --git a/pkg/services/queryhistory/queryhistory.go b/pkg/services/queryhistory/queryhistory.go index b2ddf5b5cd5..90979025d5b 100644 --- a/pkg/services/queryhistory/queryhistory.go +++ b/pkg/services/queryhistory/queryhistory.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -33,13 +32,6 @@ func ProvideService(cfg *setting.Cfg, // Register routes only when query history is enabled if s.Cfg.QueryHistoryEnabled { - //nolint:staticcheck // not yet migrated to OpenFeature - if features.IsEnabledGlobally(featuremgmt.FlagKubernetesStars) { - s.k8sClients = &k8sClients{ - namespacer: request.GetNamespaceMapper(s.Cfg), - configProvider: configProvider, - } - } s.registerAPIEndpoints() } @@ -64,7 +56,6 @@ type QueryHistoryService struct { log log.Logger now func() time.Time accessControl ac.AccessControl - k8sClients *k8sClients } func (s QueryHistoryService) CreateQueryInQueryHistory(ctx context.Context, user *user.SignedInUser, cmd CreateQueryInQueryHistoryCommand) (QueryHistoryDTO, error) { diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 3536fa68149..8d6216ef648 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -302,10 +302,6 @@ func (rs *RenderingService) render(ctx context.Context, renderType RenderType, o metrics.MRenderingQueue.Set(float64(atomic.AddInt32(&rs.inProgressCount, 1))) if renderType == RenderPDF { - if !rs.features.IsEnabled(ctx, featuremgmt.FlagNewPDFRendering) { - return nil, fmt.Errorf("feature 'newPDFRendering' disabled") - } - if err := rs.IsCapabilitySupported(ctx, PDFRendering); err != nil { return nil, err } diff --git a/pkg/services/serviceaccounts/api/token.go b/pkg/services/serviceaccounts/api/token.go index dcfb82b10f5..620d278ee1e 100644 --- a/pkg/services/serviceaccounts/api/token.go +++ b/pkg/services/serviceaccounts/api/token.go @@ -157,6 +157,10 @@ func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response. } if api.cfg.SATokenExpirationDayLimit > 0 { + if cmd.SecondsToLive == 0 { + return response.Error(http.StatusBadRequest, "Cannot create token with no expiration date when service_accounts.token_expiration_day_limit is set", nil) + } + dayExpireLimit := time.Now().Add(time.Duration(api.cfg.SATokenExpirationDayLimit) * time.Hour * 24).Truncate(24 * time.Hour) expirationDate := time.Now().Add(time.Duration(cmd.SecondsToLive) * time.Second).Truncate(24 * time.Hour) if expirationDate.After(dayExpireLimit) { diff --git a/pkg/services/serviceaccounts/api/token_test.go b/pkg/services/serviceaccounts/api/token_test.go index 087d786b723..e6218c10638 100644 --- a/pkg/services/serviceaccounts/api/token_test.go +++ b/pkg/services/serviceaccounts/api/token_test.go @@ -60,14 +60,15 @@ func TestServiceAccountsAPI_ListTokens(t *testing.T) { func TestServiceAccountsAPI_CreateToken(t *testing.T) { type TestCase struct { - desc string - id int64 - body string - permissions []accesscontrol.Permission - tokenTTL int64 - expectedErr error - expectedAPIKey *apikey.APIKey - expectedCode int + desc string + id int64 + body string + permissions []accesscontrol.Permission + tokenTTL int64 + tokenExpirationDayLimit int + expectedErr error + expectedAPIKey *apikey.APIKey + expectedCode int } tests := []TestCase{ @@ -105,12 +106,58 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) { permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, expectedCode: http.StatusBadRequest, }, + { + desc: "should not be able to create token for service account if max ttl is configured and exceeds the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 11000}`, + tokenTTL: 10000, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedCode: http.StatusBadRequest, + }, + { + desc: "should be able to create token for service account if max ttl is configured and within the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 5000}`, + tokenTTL: 10000, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedAPIKey: &apikey.APIKey{}, + expectedCode: http.StatusOK, + }, + { + desc: "should not be able to create token for service account if token expiration day limit is configured but not set in body", + id: 1, + body: `{"name": "test"}`, + tokenTTL: -1, + tokenExpirationDayLimit: 30, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedCode: http.StatusBadRequest, + }, + { + desc: "should not be able to create token for service account if token expiration day limit is configured and exceeds the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 340000}`, // 340000 seconds is > 3 days + tokenTTL: -1, + tokenExpirationDayLimit: 3, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedCode: http.StatusBadRequest, + }, + { + desc: "should be able to create token for service account if token expiration day limit is configured and within the limit", + id: 1, + body: `{"name": "test", "secondsToLive": 250000}`, // 250000 seconds is almost 3 days + tokenTTL: -1, + tokenExpirationDayLimit: 3, + permissions: []accesscontrol.Permission{{Action: serviceaccounts.ActionWrite, Scope: "serviceaccounts:id:1"}}, + expectedAPIKey: &apikey.APIKey{}, + expectedCode: http.StatusOK, + }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { server := setupTests(t, func(a *ServiceAccountsAPI) { a.cfg.ApiKeyMaxSecondsToLive = tt.tokenTTL + a.cfg.SATokenExpirationDayLimit = tt.tokenExpirationDayLimit a.service = &satests.FakeServiceAccountService{ ExpectedErr: tt.expectedErr, ExpectedAPIKey: tt.expectedAPIKey, diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index d6d98ebfec9..706b8dcf026 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -24,6 +24,9 @@ func AddTablesMigrations(mg *migrator.Migrator) { mg.AddMigration("add last_applied column to alert_configuration_history", migrator.NewAddColumnMigration(migrator.Table{Name: "alert_configuration_history"}, &migrator.Column{ Name: "last_applied", Type: migrator.DB_Int, Nullable: false, Default: "0", })) + mg.AddMigration("add message column to alert_rule_version", migrator.NewAddColumnMigration(migrator.Table{Name: "alert_rule_version"}, &migrator.Column{ + Name: "message", Type: migrator.DB_Text, Nullable: true, + })) // End of migration log, add new migrations above this line. } diff --git a/pkg/services/star/api/client.go b/pkg/services/star/api/client.go index 2f4cd238cc7..661455b02ef 100644 --- a/pkg/services/star/api/client.go +++ b/pkg/services/star/api/client.go @@ -9,8 +9,8 @@ import ( "k8s.io/client-go/kubernetes" authlib "github.com/grafana/authlib/types" + collectionsV1 "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" dashboardsV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - preferencesV1 "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -59,7 +59,7 @@ func (k *k8sClients) GetStars(c *contextmodel.ReqContext) ([]string, error) { if err != nil { return nil, err } - client := dyn.Resource(preferencesV1.StarsResourceInfo.GroupVersionResource()).Namespace(k.namespacer(c.OrgID)) + client := dyn.Resource(collectionsV1.StarsResourceInfo.GroupVersionResource()).Namespace(k.namespacer(c.OrgID)) ctx := c.Req.Context() user, err := identity.GetRequester(ctx) @@ -104,7 +104,7 @@ func (k *k8sClients) AddStar(c *contextmodel.ReqContext, uid string) error { client := dyn.RESTClient() rsp := client.Put().AbsPath( - "apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns, + "apis", collectionsV1.APIGroup, collectionsV1.APIVersion, "namespaces", ns, "stars", "user-"+user.GetIdentifier(), "update", dashboardsV1.APIGroup, dashboardsV1.DashboardKind().Kind(), uid, ).Do(ctx) @@ -129,7 +129,7 @@ func (k *k8sClients) RemoveStar(c *contextmodel.ReqContext, uid string) error { client := dyn.RESTClient() rsp := client.Delete().AbsPath( - "apis", preferencesV1.APIGroup, preferencesV1.APIVersion, "namespaces", ns, + "apis", collectionsV1.APIGroup, collectionsV1.APIVersion, "namespaces", ns, "stars", "user-"+user.GetIdentifier(), "update", dashboardsV1.APIGroup, dashboardsV1.DashboardKind().Kind(), uid, ).Do(ctx) diff --git a/pkg/services/store/kind/dashboard/dashboard.go b/pkg/services/store/kind/dashboard/dashboard.go index 42a2e1e5067..656eefc15f3 100644 --- a/pkg/services/store/kind/dashboard/dashboard.go +++ b/pkg/services/store/kind/dashboard/dashboard.go @@ -204,7 +204,13 @@ func readDashboardIter(jsonPath string, iter *jsoniter.Iterator, lookup Datasour } case "refresh": - if !checkAndSkipUnexpectedElement(iter, jsonPath+".refresh", lc, jsoniter.StringValue) { + // "refresh" used to be boolean. We will silently skip it in such case. + if !checkAndSkipUnexpectedElement(iter, jsonPath+".refresh", lc, jsoniter.StringValue, jsoniter.BoolValue) { + continue + } + + if iter.WhatIsNext() == jsoniter.BoolValue { + iter.Skip() continue } dash.Refresh = iter.ReadString() diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 885ea5703ec..fb69aa60259 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -480,6 +480,9 @@ type Cfg struct { // LiveMessageSizeLimit is the maximum size in bytes of Websocket messages // from clients. Defaults to 64KB. LiveMessageSizeLimit int + // LiveClientQueueMaxSize is the maximum size in bytes of the client queue + // for Live connections. Defaults to 4MB. + LiveClientQueueMaxSize int // Grafana.com URL, used for OAuth redirect. GrafanaComURL string @@ -2066,6 +2069,11 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { if cfg.LiveMessageSizeLimit < -1 { return fmt.Errorf("unexpected value %d for [live] message_size_limit", cfg.LiveMaxConnections) } + cfg.LiveClientQueueMaxSize = section.Key("client_queue_max_size").MustInt(4194304) + if cfg.LiveClientQueueMaxSize <= 0 { + return fmt.Errorf("unexpected value %d for [live] client_queue_max_size", cfg.LiveMaxConnections) + } + cfg.LiveHAEngine = section.Key("ha_engine").MustString("") switch cfg.LiveHAEngine { case "", "redis": diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 0a4d3e630ff..07165937199 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -57,7 +57,6 @@ func ProvideUnifiedStorageClient(opts *Options, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, ) (resource.ResourceClient, error) { - // See: apiserver.applyAPIServerConfig(cfg, features, o) apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver") client, err := newClient(options.StorageOptions{ StorageType: options.StorageType(apiserverCfg.Key("storage_type").MustString(string(options.StorageTypeUnified))), @@ -165,12 +164,8 @@ func newClient(opts options.StorageOptions, indexConn = conn } - // Create a client instance - client, err := resource.NewResourceClient(conn, indexConn, cfg, features, tracer) - if err != nil { - return nil, err - } - return client, nil + // Create a resource client + return resource.NewResourceClient(conn, indexConn, cfg, features, tracer) default: searchOptions, err := search.NewSearchOptions(features, cfg, tracer, docs, indexMetrics, nil) diff --git a/pkg/storage/unified/migrations/dashboard_folder_migration.go b/pkg/storage/unified/migrations/dashboard_folder_migration.go new file mode 100644 index 00000000000..96c5a824c8a --- /dev/null +++ b/pkg/storage/unified/migrations/dashboard_folder_migration.go @@ -0,0 +1,93 @@ +package migrations + +import ( + "context" + "fmt" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/util/xorm" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + FoldersAndDashboardsMigrationID = "folders and dashboards migration" + UnifiedStorageDataMigrationSQL = "unified storage data migration" +) + +type dashboardAndFolderMigration struct { + migrator.MigrationBase + legacyMigrator legacy.LegacyMigrator + bulkStoreClient resource.ResourceClient +} + +var _ migrator.CodeMigration = (*dashboardAndFolderMigration)(nil) + +// SQL implements migrator.Migration interface. Returns a description string. +func (sp *dashboardAndFolderMigration) SQL(dialect migrator.Dialect) string { + return UnifiedStorageDataMigrationSQL +} + +func (sp *dashboardAndFolderMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + ctx := context.Background() + logger := mg.Logger + + resources := []schema.GroupResource{ + { + Group: "folder.grafana.app", + Resource: "folders", + }, + { + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + } + + storageMigrator := newUnifiedStorageMigrator(sp.legacyMigrator, sp.bulkStoreClient, resources, "unified-storage-migration.folders-dashboards") + + orgs, err := sp.getAllOrgs(sess) + if err != nil { + logger.Error("failed to get organizations for folders and dashboards migration", "error", err) + return fmt.Errorf("failed to get organizations: %w", err) + } + + if len(orgs) == 0 { + logger.Info("No organizations found to migrate, skipping migration") + return nil + } + + logger.Info("Starting migration for all organizations", "org_count", len(orgs)) + + for _, org := range orgs { + namespace := types.OrgNamespaceFormatter(org.ID) + logger.Info("Migrating organization", "org_id", org.ID, "org_name", org.Name, "namespace", namespace) + + // Create a service identity context for this namespace to authenticate with unified storage + migrationCtx, _ := identity.WithServiceIdentityForSingleNamespace(ctx, namespace) + + if err := storageMigrator.executeMigration(migrationCtx, sess, mg, namespace); err != nil { + logger.Error("migration failed for organization", "org_id", org.ID, "org_name", org.Name, "error", err) + return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) + } + } + + logger.Info("Migration completed successfully for all organizations", "org_count", len(orgs)) + return nil +} + +type orgInfo struct { + ID int64 `xorm:"id"` + Name string `xorm:"name"` +} + +func (sp *dashboardAndFolderMigration) getAllOrgs(sess *xorm.Session) ([]orgInfo, error) { + var orgs []orgInfo + err := sess.Table("org").Cols("id", "name").Find(&orgs) + if err != nil { + return nil, err + } + return orgs, nil +} diff --git a/pkg/storage/unified/migrations/migrations.go b/pkg/storage/unified/migrations/migrations.go new file mode 100644 index 00000000000..195b7f5d154 --- /dev/null +++ b/pkg/storage/unified/migrations/migrations.go @@ -0,0 +1,108 @@ +package migrations + +import ( + "context" + "fmt" + "os" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" +) + +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/storage/unified/migrations") + +// UnifiedStorageMigrationProvider provides unified storage migrations as a background service +type UnifiedStorageMigrationProvider interface { + registry.BackgroundService +} + +type UnifiedStorageMigrationProviderImpl struct { + legacyMigrator legacy.LegacyMigrator + cfg *setting.Cfg + client resource.ResourceClient + sqlStore db.DB +} + +var _ UnifiedStorageMigrationProvider = (*UnifiedStorageMigrationProviderImpl)(nil) + +// ProvideUnifiedStorageMigrationProvider is a Wire provider that creates the migration service. +// The service implements registry.BackgroundService and runs migrations during server startup. +func ProvideUnifiedStorageMigrationProvider( + legacyMigrator legacy.LegacyMigrator, + cfg *setting.Cfg, + client resource.ResourceClient, + sqlStore db.DB, +) *UnifiedStorageMigrationProviderImpl { + return &UnifiedStorageMigrationProviderImpl{ + legacyMigrator: legacyMigrator, + cfg: cfg, + client: client, + sqlStore: sqlStore, + } +} + +// Run executes unified storage migrations as a background service. +// This blocks until migrations complete. If migrations fail, an error is returned +// which will prevent Grafana from starting. +func (p *UnifiedStorageMigrationProviderImpl) Run(ctx context.Context) error { + // skip migrations in test environments to prevent integration test timeouts. + if os.Getenv("GRAFANA_TEST_DB") != "" { + return nil + } + + // TODO: Re-enable once migrations are ready + // return RegisterMigrations(p.legacyMigrator, p.cfg, p.client, p.sqlStore) + return nil +} + +// RegisterMigrations initializes and registers all unified storage migrations. +// This function is the entry point for all data migrations from legacy storage +// to unified storage. It returns an error if migrations fail, preventing Grafana +// from starting with inconsistent data. +func RegisterMigrations( + legacyMigrator legacy.LegacyMigrator, + cfg *setting.Cfg, + client resource.ResourceClient, + sqlStore db.DB, +) error { + ctx, span := tracer.Start(context.Background(), "storage.unified.RegisterMigrations") + defer span.End() + logger := log.New("storage.unified.migrations.folders-dashboards") + mg := migrator.NewScopedMigrator(sqlStore.GetEngine(), cfg, "unified_storage") + mg.AddCreateMigration() + + if err := prometheus.Register(mg); err != nil { + logger.Warn("Failed to register migrator metrics", "error", err) + } + + // Add new migration registrations here for each resource type + registerDashboardAndFolderMigration(mg, legacyMigrator, client) + + // Run all registered migrations (blocking) + sec := cfg.Raw.Section("database") + if err := mg.RunMigrations(ctx, sec.Key("migration_locking").MustBool(true), sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { + return fmt.Errorf("unified storage data migration failed: %w", err) + } + + logger.Info("Unified storage migrations completed successfully") + return nil +} + +func registerDashboardAndFolderMigration( + mg *migrator.Migrator, + legacyMigrator legacy.LegacyMigrator, + bulkStoreClient resource.ResourceClient, +) { + migration := &dashboardAndFolderMigration{ + legacyMigrator: legacyMigrator, + bulkStoreClient: bulkStoreClient, + } + mg.AddMigration(FoldersAndDashboardsMigrationID, migration) +} diff --git a/pkg/storage/unified/migrations/migrator.go b/pkg/storage/unified/migrations/migrator.go new file mode 100644 index 00000000000..cab6ce38432 --- /dev/null +++ b/pkg/storage/unified/migrations/migrator.go @@ -0,0 +1,166 @@ +package migrations + +import ( + "context" + "fmt" + "time" + + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util/xorm" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// StorageMigrator defines the interface for executing unified storage migrations +type StorageMigrator interface { + executeMigration(ctx context.Context, sess *xorm.Session, mg *migrator.Migrator, namespace string) error +} + +type unifiedStorageMigrator struct { + migrator legacy.LegacyMigrator + bulkStoreClient resource.ResourceClient + resources []schema.GroupResource + log log.Logger +} + +func newUnifiedStorageMigrator(migrator legacy.LegacyMigrator, bulkStoreClient resource.ResourceClient, resources []schema.GroupResource, logPrefix string) StorageMigrator { + return &unifiedStorageMigrator{ + migrator: migrator, + bulkStoreClient: bulkStoreClient, + resources: resources, + log: log.New(logPrefix), + } +} + +func (m *unifiedStorageMigrator) executeMigration(ctx context.Context, sess *xorm.Session, mg *migrator.Migrator, namespace string) error { + startTime := time.Now() + m.log.Info("Starting unified storage migration", "namespace", namespace, "resources", m.resources) + + opts := legacy.MigrateOptions{ + Namespace: namespace, + Store: m.bulkStoreClient, + LargeObjects: nil, // Not using large object support to avoid import cycles + Resources: m.resources, + WithHistory: true, // Migrate with full history + OnlyCount: false, + Progress: func(count int, msg string) { + m.log.Info("Migration progress", "count", count, "message", msg) + }, + } + + // Execute the migration via legacy migrator + response, err := m.migrator.Migrate(ctx, opts) + if err != nil { + m.log.Error("Migration failed", "error", err, "duration", time.Since(startTime)) + return fmt.Errorf("failed to migrate resources: %w", err) + } + + // Validate the migration results + if err := m.validateMigration(sess, response); err != nil { + m.log.Error("Migration validation failed", "error", err, "duration", time.Since(startTime)) + return fmt.Errorf("migration validation failed: %w", err) + } + + m.log.Info("Migration completed successfully", + "duration", time.Since(startTime), + "processed", response.Processed, + "summaries", len(response.Summary), + "rejected", len(response.Rejected)) + + return nil +} + +func (m *unifiedStorageMigrator) validateMigration(sess *xorm.Session, response *resourcepb.BulkResponse) error { + // Check for rejected items + if len(response.Rejected) > 0 { + m.log.Warn("Migration had rejected items", "count", len(response.Rejected)) + for i, rejected := range response.Rejected { + if i < 10 { // Log first 10 rejected items + m.log.Warn("Rejected item", + "namespace", rejected.Key.Namespace, + "group", rejected.Key.Group, + "resource", rejected.Key.Resource, + "name", rejected.Key.Name, + "reason", rejected.Error) + } + } + // Rejections are not fatal - they may be expected for invalid data + } + + // Validate counts for each resource type + for _, summary := range response.Summary { + legacyCount, err := m.getLegacyCount(sess, summary.Group, summary.Resource, summary.Namespace) + if err != nil { + return fmt.Errorf("failed to get legacy count for %s/%s: %w", summary.Group, summary.Resource, err) + } + + // Account for rejected items in validation + expectedCount := summary.Count + int64(len(response.Rejected)) + + m.log.Info("Count validation", + "resource", fmt.Sprintf("%s.%s", summary.Resource, summary.Group), + "namespace", summary.Namespace, + "legacy_count", legacyCount, + "unified_count", summary.Count, + "rejected", len(response.Rejected), + "history", summary.History) + + // Validate that we migrated all items (allowing for rejected items) + if legacyCount > expectedCount { + return fmt.Errorf("count mismatch for %s.%s in namespace %s: legacy has %d, unified has %d, rejected %d", + summary.Resource, summary.Group, summary.Namespace, + legacyCount, summary.Count, len(response.Rejected)) + } + } + + return nil +} + +func (m *unifiedStorageMigrator) getLegacyCount(sess *xorm.Session, group, resourceType, namespace string) (int64, error) { + // Parse namespace to get org ID + orgID, err := ParseOrgIDFromNamespace(namespace) + if err != nil { + return 0, fmt.Errorf("invalid namespace %s: %w", namespace, err) + } + + // Map group/resource to legacy table + tableName, whereClause := m.getLegacyTableInfo(group, resourceType) + if tableName == "" { + return 0, fmt.Errorf("unknown resource type: %s.%s", resourceType, group) + } + + // Count items in legacy table using Table() before Count() + count, err := sess.Table(tableName).Where(whereClause, orgID).Count() + if err != nil { + return 0, fmt.Errorf("failed to count %s: %w", tableName, err) + } + + return count, nil +} + +func (m *unifiedStorageMigrator) getLegacyTableInfo(group, resource string) (table string, whereClause string) { + // Map unified storage group/resource to legacy tables + switch { + case group == "dashboard.grafana.app" && resource == "dashboards": + return "dashboard", "org_id = ? and is_folder = false" + case group == "folder.grafana.app" && resource == "folders": + return "dashboard", "org_id = ? and is_folder = true" + case group == "playlist.grafana.app" && resource == "playlists": + return "playlist", "org_id = ?" + default: + return "", "" + } +} + +func ParseOrgIDFromNamespace(namespace string) (int64, error) { + // Use authlib to properly parse all namespace formats including "default" for org 1 + info, err := types.ParseNamespace(namespace) + if err != nil { + return 0, fmt.Errorf("failed to parse namespace: %w", err) + } + return info.OrgID, nil +} diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 7b5a6544e5b..2752c59650d 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -75,16 +75,16 @@ type ResourceIndex interface { // Search within a namespaced resource // When working with federated queries, the additional indexes will be passed in explicitly - Search(ctx context.Context, access types.AccessClient, req *resourcepb.ResourceSearchRequest, federate []ResourceIndex) (*resourcepb.ResourceSearchResponse, error) + Search(ctx context.Context, access types.AccessClient, req *resourcepb.ResourceSearchRequest, federate []ResourceIndex, stats *SearchStats) (*resourcepb.ResourceSearchResponse, error) // List within an response - ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest) (*resourcepb.ListManagedObjectsResponse, error) + ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest, stats *SearchStats) (*resourcepb.ListManagedObjectsResponse, error) // Counts the values in a repo - CountManagedObjects(ctx context.Context) ([]*resourcepb.CountManagedObjectsResponse_ResourceCount, error) + CountManagedObjects(ctx context.Context, stats *SearchStats) ([]*resourcepb.CountManagedObjectsResponse_ResourceCount, error) // Get the number of documents in the index - DocCount(ctx context.Context, folder string) (int64, error) + DocCount(ctx context.Context, folder string, stats *SearchStats) (int64, error) // UpdateIndex updates the index with the latest data (using update function provided when index was built) to guarantee strong consistency during the search. // Returns RV to which index was updated. @@ -238,6 +238,9 @@ func combineRebuildRequests(a, b rebuildRequest) (c rebuildRequest, ok bool) { } func (s *searchSupport) ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest) (*resourcepb.ListManagedObjectsResponse, error) { + ctx, span := tracer.Start(ctx, "resource.searchSupport.ListManagedObjects") + defer span.End() + if req.NextPageToken != "" { return &resourcepb.ListManagedObjectsResponse{ Error: NewBadRequestError("multiple pages not yet supported"), @@ -248,14 +251,17 @@ func (s *searchSupport) ListManagedObjects(ctx context.Context, req *resourcepb. nsr := NamespacedResource{ Namespace: req.Namespace, } - stats, err := s.storage.GetResourceStats(ctx, nsr, 0) + resourceStats, err := s.storage.GetResourceStats(ctx, nsr, 0) if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil } - for _, info := range stats { - idx, err := s.getOrCreateIndex(ctx, NamespacedResource{ + stats := NewSearchStats("ListManagedObjects") + defer s.logStats(ctx, stats, span, "namespace", req.Namespace) + + for _, info := range resourceStats { + idx, err := s.getOrCreateIndex(ctx, stats, NamespacedResource{ Namespace: req.Namespace, Group: info.Group, Resource: info.Resource, @@ -265,7 +271,7 @@ func (s *searchSupport) ListManagedObjects(ctx context.Context, req *resourcepb. return rsp, nil } - kind, err := idx.ListManagedObjects(ctx, req) + kind, err := idx.ListManagedObjects(ctx, req, stats) if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -280,26 +286,69 @@ func (s *searchSupport) ListManagedObjects(ctx context.Context, req *resourcepb. } // Sort based on path + start := time.Now() slices.SortFunc(rsp.Items, func(a, b *resourcepb.ListManagedObjectsResponse_Item) int { return cmp.Compare(a.Path, b.Path) }) + stats.AddResultsConversionTime(time.Since(start)) return rsp, nil } +func (s *searchSupport) logWithTraceID(ctx context.Context) *slog.Logger { + l := s.log + if traceID := tracing.TraceIDFromContext(ctx, false); traceID != "" { + l = l.With("traceID", traceID) + } + return l +} + +func (s *searchSupport) logStats(ctx context.Context, stats *SearchStats, span trace.Span, params ...any) { + elapsed := time.Since(stats.startTime) + + args := []any{ + "operation", stats.operation, + "elapsedTime", elapsed, + "indexBuildTime", stats.indexBuildTime, + "indexUpdateTime", stats.indexUpdateTime, + "requestConversionTime", stats.requestConversion, + "searchTime", stats.searchTime, + "totalHits", stats.totalHits, + "returnedDocuments", stats.returnedDocuments, + "resultsConversionTime", stats.resultsConversionTime, + } + args = append(args, params...) + + s.logWithTraceID(ctx).Debug("Search stats", args...) + + if span != nil { + attrs := make([]attribute.KeyValue, 0, len(args)/2) + for i := 0; i < len(args); i += 2 { + attrs = append(attrs, attribute.String(fmt.Sprint(args[i]), fmt.Sprint(args[i+1]))) + } + span.AddEvent("search stats", trace.WithAttributes(attrs...)) + } +} + func (s *searchSupport) CountManagedObjects(ctx context.Context, req *resourcepb.CountManagedObjectsRequest) (*resourcepb.CountManagedObjectsResponse, error) { + ctx, span := tracer.Start(ctx, "resource.searchSupport.CountManagedObjects") + defer span.End() + + stats := NewSearchStats("CountManagedObjects") + defer s.logStats(ctx, stats, span, "namespace", req.Namespace) + rsp := &resourcepb.CountManagedObjectsResponse{} nsr := NamespacedResource{ Namespace: req.Namespace, } - stats, err := s.storage.GetResourceStats(ctx, nsr, 0) + resourceStats, err := s.storage.GetResourceStats(ctx, nsr, 0) if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil } - for _, info := range stats { - idx, err := s.getOrCreateIndex(ctx, NamespacedResource{ + for _, info := range resourceStats { + idx, err := s.getOrCreateIndex(ctx, stats, NamespacedResource{ Namespace: req.Namespace, Group: info.Group, Resource: info.Resource, @@ -309,7 +358,7 @@ func (s *searchSupport) CountManagedObjects(ctx context.Context, req *resourcepb return rsp, nil } - counts, err := idx.CountManagedObjects(ctx) + counts, err := idx.CountManagedObjects(ctx, stats) if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -349,12 +398,15 @@ func (s *searchSupport) Search(ctx context.Context, req *resourcepb.ResourceSear }, nil } + stats := NewSearchStats("Search") + defer s.logStats(ctx, stats, span, "namespace", req.Options.Key.Namespace, "group", req.Options.Key.Group, "resource", req.Options.Key.Resource, "query", req.Query) + nsr := NamespacedResource{ Group: req.Options.Key.Group, Namespace: req.Options.Key.Namespace, Resource: req.Options.Key.Resource, } - idx, err := s.getOrCreateIndex(ctx, nsr, "search") + idx, err := s.getOrCreateIndex(ctx, stats, nsr, "search") if err != nil { return &resourcepb.ResourceSearchResponse{ Error: AsErrorResult(err), @@ -366,7 +418,7 @@ func (s *searchSupport) Search(ctx context.Context, req *resourcepb.ResourceSear for i, f := range req.Federated { nsr.Group = f.Group nsr.Resource = f.Resource - federate[i], err = s.getOrCreateIndex(ctx, nsr, "federatedSearch") + federate[i], err = s.getOrCreateIndex(ctx, stats, nsr, "federatedSearch") if err != nil { return &resourcepb.ResourceSearchResponse{ Error: AsErrorResult(err), @@ -374,16 +426,23 @@ func (s *searchSupport) Search(ctx context.Context, req *resourcepb.ResourceSear } } - return idx.Search(ctx, s.access, req, federate) + return idx.Search(ctx, s.access, req, federate, stats) } // GetStats implements ResourceServer. func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest) (*resourcepb.ResourceStatsResponse, error) { + ctx, span := tracer.Start(ctx, "resource.searchSupport.GetStats") + defer span.End() + if req.Namespace == "" { return &resourcepb.ResourceStatsResponse{ Error: NewBadRequestError("missing namespace"), }, nil } + + stats := NewSearchStats("GetStats") + defer s.logStats(ctx, stats, span, "namespace", req.Namespace, "group", strings.Join(req.Kinds, ","), "folder", req.Folder) + rsp := &resourcepb.ResourceStatsResponse{} // Explicit list of kinds @@ -391,7 +450,7 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt rsp.Stats = make([]*resourcepb.ResourceStatsResponse_Stats, len(req.Kinds)) for i, k := range req.Kinds { parts := strings.SplitN(k, "/", 2) - index, err := s.getOrCreateIndex(ctx, NamespacedResource{ + index, err := s.getOrCreateIndex(ctx, stats, NamespacedResource{ Namespace: req.Namespace, Group: parts[0], Resource: parts[1], @@ -400,7 +459,7 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt rsp.Error = AsErrorResult(err) return rsp, nil } - count, err := index.DocCount(ctx, req.Folder) + count, err := index.DocCount(ctx, req.Folder, stats) if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -417,17 +476,17 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt nsr := NamespacedResource{ Namespace: req.Namespace, } - stats, err := s.storage.GetResourceStats(ctx, nsr, 0) + resourceStats, err := s.storage.GetResourceStats(ctx, nsr, 0) if err != nil { return &resourcepb.ResourceStatsResponse{ Error: AsErrorResult(err), }, nil } - rsp.Stats = make([]*resourcepb.ResourceStatsResponse_Stats, len(stats)) + rsp.Stats = make([]*resourcepb.ResourceStatsResponse_Stats, len(resourceStats)) // When not filtered by folder or repository, we can use the results directly if req.Folder == "" { - for i, stat := range stats { + for i, stat := range resourceStats { rsp.Stats[i] = &resourcepb.ResourceStatsResponse_Stats{ Group: stat.Group, Resource: stat.Resource, @@ -437,8 +496,8 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt return rsp, nil } - for i, stat := range stats { - index, err := s.getOrCreateIndex(ctx, NamespacedResource{ + for i, stat := range resourceStats { + index, err := s.getOrCreateIndex(ctx, stats, NamespacedResource{ Namespace: req.Namespace, Group: stat.Group, Resource: stat.Resource, @@ -447,7 +506,7 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt rsp.Error = AsErrorResult(err) return rsp, nil } - count, err := index.DocCount(ctx, req.Folder) + count, err := index.DocCount(ctx, req.Folder, stats) if err != nil { rsp.Error = AsErrorResult(err) return rsp, nil @@ -733,7 +792,7 @@ type rebuildRequest struct { minBuildVersion *semver.Version // if not nil, rebuild index with build version older than this. } -func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedResource, reason string) (ResourceIndex, error) { +func (s *searchSupport) getOrCreateIndex(ctx context.Context, stats *SearchStats, key NamespacedResource, reason string) (ResourceIndex, error) { if s == nil || s.search == nil { return nil, fmt.Errorf("search is not configured properly (missing unifiedStorageSearch feature toggle?)") } @@ -750,6 +809,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso idx := s.search.GetIndex(key) if idx == nil { span.AddEvent("Building index") + buildStartTime := time.Now() ch := s.buildIndex.DoChan(key.String(), func() (interface{}, error) { // We want to finish building of the index even if original context is canceled. // We reuse original context without cancel to keep the tracing spans correct. @@ -795,6 +855,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso if res.Err != nil { return nil, tracing.Error(span, res.Err) } + stats.AddIndexBuildTime(time.Since(buildStartTime)) idx = res.Val.(ResourceIndex) case <-ctx.Done(): return nil, tracing.Error(span, fmt.Errorf("failed to get index: %w", ctx.Err())) @@ -808,10 +869,11 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso return nil, tracing.Error(span, fmt.Errorf("failed to update index to guarantee strong consistency: %w", err)) } elapsed := time.Since(start) + stats.AddIndexUpdateTime(elapsed) if s.indexMetrics != nil { s.indexMetrics.SearchUpdateWaitTime.WithLabelValues(reason).Observe(elapsed.Seconds()) } - s.log.Debug("Index updated before search", "namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "reason", reason, "duration", elapsed, "rv", rv) + s.logWithTraceID(ctx).Debug("Index updated before search", "namespace", key.Namespace, "group", key.Group, "resource", key.Resource, "reason", reason, "duration", elapsed, "rv", rv) span.AddEvent("Index updated") return idx, nil @@ -828,7 +890,7 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size attribute.Int64("size", size), ) - logger := s.log.With("namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) + logger := s.logWithTraceID(ctx).With("namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource) builder, err := s.builders.get(ctx, nsr) if err != nil { @@ -991,7 +1053,9 @@ func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size } // Record the number of objects indexed for the kind/resource - docCount, err := index.DocCount(ctx, "") + // We don't pass searchStats to DocCount here, as it's not really user-initiated search. Time spent + // here will be recorded in the index build time instead. + docCount, err := index.DocCount(ctx, "", nil) if err != nil { logger.Warn("error getting doc count", "error", err) } diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index c3f852c837e..215a88cd2cd 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -41,22 +41,22 @@ func (m *MockResourceIndex) BulkIndex(req *BulkIndexRequest) error { return args.Error(0) } -func (m *MockResourceIndex) Search(ctx context.Context, access types.AccessClient, req *resourcepb.ResourceSearchRequest, federate []ResourceIndex) (*resourcepb.ResourceSearchResponse, error) { +func (m *MockResourceIndex) Search(ctx context.Context, access types.AccessClient, req *resourcepb.ResourceSearchRequest, federate []ResourceIndex, stats *SearchStats) (*resourcepb.ResourceSearchResponse, error) { args := m.Called(ctx, access, req, federate) return args.Get(0).(*resourcepb.ResourceSearchResponse), args.Error(1) } -func (m *MockResourceIndex) CountManagedObjects(ctx context.Context) ([]*resourcepb.CountManagedObjectsResponse_ResourceCount, error) { +func (m *MockResourceIndex) CountManagedObjects(ctx context.Context, stats *SearchStats) ([]*resourcepb.CountManagedObjectsResponse_ResourceCount, error) { args := m.Called(ctx) return args.Get(0).([]*resourcepb.CountManagedObjectsResponse_ResourceCount), args.Error(1) } -func (m *MockResourceIndex) DocCount(ctx context.Context, folder string) (int64, error) { +func (m *MockResourceIndex) DocCount(ctx context.Context, folder string, stats *SearchStats) (int64, error) { args := m.Called(ctx, folder) return args.Get(0).(int64), args.Error(1) } -func (m *MockResourceIndex) ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest) (*resourcepb.ListManagedObjectsResponse, error) { +func (m *MockResourceIndex) ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest, stats *SearchStats) (*resourcepb.ListManagedObjectsResponse, error) { args := m.Called(ctx, req) return args.Get(0).(*resourcepb.ListManagedObjectsResponse), args.Error(1) } @@ -223,7 +223,7 @@ func TestSearchGetOrCreateIndex(t *testing.T) { go func() { defer wg.Done() <-start - _, _ = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "test") + _, _ = support.getOrCreateIndex(context.Background(), nil, NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "test") }() } @@ -270,17 +270,17 @@ func TestSearchGetOrCreateIndexWithIndexUpdate(t *testing.T) { require.NoError(t, err) require.NotNil(t, support) - idx, err := support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "initial call") + idx, err := support.getOrCreateIndex(context.Background(), nil, NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "initial call") require.NoError(t, err) require.NotNil(t, idx) checkMockIndexUpdateCalls(t, idx, 1) - idx, err = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "second call") + idx, err = support.getOrCreateIndex(context.Background(), nil, NamespacedResource{Namespace: "ns", Group: "group", Resource: "resource"}, "second call") require.NoError(t, err) require.NotNil(t, idx) checkMockIndexUpdateCalls(t, idx, 2) - idx, err = support.getOrCreateIndex(context.Background(), NamespacedResource{Namespace: "ns", Group: "group", Resource: "bad"}, "call to bad index") + idx, err = support.getOrCreateIndex(context.Background(), nil, NamespacedResource{Namespace: "ns", Group: "group", Resource: "bad"}, "call to bad index") require.ErrorIs(t, err, failedErr) require.Nil(t, idx) } @@ -324,7 +324,7 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() - _, err = support.getOrCreateIndex(ctx, key, "test") + _, err = support.getOrCreateIndex(ctx, nil, key, "test") // Make sure we get context deadline error require.ErrorIs(t, err, context.DeadlineExceeded) @@ -340,7 +340,7 @@ func TestSearchGetOrCreateIndexWithCancellation(t *testing.T) { }, 1*time.Second, 100*time.Millisecond, "Indexing finishes despite context cancellation") // Second call to getOrCreateIndex returns index immediately, even if context is canceled, as the index is now ready and cached. - _, err = support.getOrCreateIndex(ctx, key, "test") + _, err = support.getOrCreateIndex(ctx, nil, key, "test") require.NoError(t, err) } diff --git a/pkg/storage/unified/resource/stats.go b/pkg/storage/unified/resource/stats.go new file mode 100644 index 00000000000..f35bfd4f4ed --- /dev/null +++ b/pkg/storage/unified/resource/stats.go @@ -0,0 +1,65 @@ +package resource + +import "time" + +type SearchStats struct { + operation string + startTime time.Time // Time when the operation. + + indexBuildTime time.Duration // Time to build indexes if it wasn't open before. + indexUpdateTime time.Duration // Time to update indexes to the latest state. + + requestConversion time.Duration // How long does it take to convert search request to the bleve query + + searchTime time.Duration + totalHits int // Total hits across all pages + returnedDocuments int // Hits returned in this page + + resultsConversionTime time.Duration // How long does it take to convert search results to final results. +} + +func NewSearchStats(op string) *SearchStats { + return &SearchStats{operation: op, startTime: time.Now()} +} + +func (s *SearchStats) AddIndexBuildTime(d time.Duration) { + if s != nil { + s.indexBuildTime += d + } +} + +func (s *SearchStats) AddIndexUpdateTime(d time.Duration) { + if s != nil { + s.indexUpdateTime += d + } +} + +func (s *SearchStats) AddRequestConversionTime(d time.Duration) { + if s != nil { + s.requestConversion += d + } +} + +func (s *SearchStats) AddSearchTime(d time.Duration) { + if s != nil { + s.searchTime += d + } +} + +func (s *SearchStats) AddTotalHits(total int) { + if s != nil { + s.totalHits += total + } +} + +func (s *SearchStats) AddReturnedDocuments(docs int) { + if s != nil { + s.returnedDocuments += docs + } +} + +func (s *SearchStats) AddResultsConversionTime(d time.Duration) { + if s != nil { + s.resultsConversionTime += d + } +} diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 863c349186f..836c491b3c7 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -866,7 +866,7 @@ func (b *bleveIndex) BuildInfo() (resource.IndexBuildInfo, error) { }, nil } -func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest) (*resourcepb.ListManagedObjectsResponse, error) { +func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resourcepb.ListManagedObjectsRequest, stats *resource.SearchStats) (*resourcepb.ListManagedObjectsResponse, error) { if req.NextPageToken != "" { return nil, fmt.Errorf("next page not implemented yet") } @@ -881,6 +881,7 @@ func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resourcepb.Lis }, nil } + start := time.Now() q := bleve.NewBooleanQuery() q.AddMust(&query.TermQuery{ Term: req.Kind, @@ -890,6 +891,7 @@ func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resourcepb.Lis Term: req.Id, FieldVal: resource.SEARCH_FIELD_MANAGER_ID, }) + stats.AddResultsConversionTime(time.Since(start)) found, err := b.index.SearchInContext(ctx, &bleve.SearchRequest{ Query: q, @@ -916,6 +918,10 @@ func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resourcepb.Lis return nil, err } + stats.AddTotalHits(int(found.Total)) + stats.AddSearchTime(found.Took) + stats.AddReturnedDocuments(len(found.Hits)) + asString := func(v any) string { if v == nil { return "" @@ -947,6 +953,7 @@ func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resourcepb.Lis return 0 } + start = time.Now() rsp := &resourcepb.ListManagedObjectsResponse{} for _, hit := range found.Hits { item := &resourcepb.ListManagedObjectsResponse_Item{ @@ -963,10 +970,11 @@ func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resourcepb.Lis } rsp.Items = append(rsp.Items, item) } + stats.AddResultsConversionTime(time.Since(start)) return rsp, nil } -func (b *bleveIndex) CountManagedObjects(ctx context.Context) ([]*resourcepb.CountManagedObjectsResponse_ResourceCount, error) { +func (b *bleveIndex) CountManagedObjects(ctx context.Context, stats *resource.SearchStats) ([]*resourcepb.CountManagedObjectsResponse_ResourceCount, error) { found, err := b.index.SearchInContext(ctx, &bleve.SearchRequest{ Query: bleve.NewMatchAllQuery(), Size: 0, @@ -977,6 +985,11 @@ func (b *bleveIndex) CountManagedObjects(ctx context.Context) ([]*resourcepb.Cou if err != nil { return nil, err } + + stats.AddSearchTime(found.Took) + stats.AddTotalHits(int(found.Total)) + stats.AddReturnedDocuments(len(found.Hits)) + vals := make([]*resourcepb.CountManagedObjectsResponse_ResourceCount, 0) f, ok := found.Facets["count"] if ok && f.Terms != nil { @@ -1003,6 +1016,7 @@ func (b *bleveIndex) Search( access authlib.AccessClient, req *resourcepb.ResourceSearchRequest, federate []resource.ResourceIndex, // For federated queries, these will match the values in req.federate + stats *resource.SearchStats, ) (*resourcepb.ResourceSearchResponse, error) { ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"Search") defer span.End() @@ -1026,6 +1040,7 @@ func (b *bleveIndex) Search( return nil, err } + conversionStarts := time.Now() // convert protobuf request to bleve request searchrequest, e := b.toBleveSearchRequest(ctx, req, access) if e != nil { @@ -1050,6 +1065,7 @@ func (b *bleveIndex) Search( } } } + stats.AddRequestConversionTime(time.Since(conversionStarts)) res, err := index.SearchInContext(ctx, searchrequest) if err != nil { @@ -1059,7 +1075,11 @@ func (b *bleveIndex) Search( response.TotalHits = int64(res.Total) response.QueryCost = float64(res.Cost) response.MaxScore = res.MaxScore + stats.AddSearchTime(res.Took) + stats.AddTotalHits(int(res.Total)) + stats.AddReturnedDocuments(len(res.Hits)) + resultsConversionStart := time.Now() response.Results, err = b.hitsToTable(ctx, searchrequest.Fields, res.Hits, req.Explain) if err != nil { return nil, err @@ -1073,10 +1093,11 @@ func (b *bleveIndex) Search( } response.Facet[k] = f } + stats.AddResultsConversionTime(time.Since(resultsConversionStart)) return response, nil } -func (b *bleveIndex) DocCount(ctx context.Context, folder string) (int64, error) { +func (b *bleveIndex) DocCount(ctx context.Context, folder string, stats *resource.SearchStats) (int64, error) { ctx, span := b.tracing.Start(ctx, tracingPrexfixBleve+"DocCount") defer span.End() @@ -1097,6 +1118,10 @@ func (b *bleveIndex) DocCount(ctx context.Context, folder string) (int64, error) if rsp == nil { return 0, err } + if stats != nil { + stats.AddTotalHits(int(rsp.Total)) + stats.AddSearchTime(rsp.Took) + } return int64(rsp.Total), err } diff --git a/pkg/storage/unified/search/bleve_performance_test.go b/pkg/storage/unified/search/bleve_performance_test.go index 00e23d78259..0dfad7ac8fe 100644 --- a/pkg/storage/unified/search/bleve_performance_test.go +++ b/pkg/storage/unified/search/bleve_performance_test.go @@ -57,7 +57,7 @@ func runBenchmark(b *testing.B, testIndex resource.ResourceIndex) { var memStatsBefore, memStatsAfter runtime.MemStats runtime.ReadMemStats(&memStatsBefore) - _, err := testIndex.Search(context.Background(), nil, searchRequest, nil) + _, err := testIndex.Search(context.Background(), nil, searchRequest, nil, nil) elapsed := time.Since(start) // Calculate elapsed time runtime.ReadMemStats(&memStatsAfter) diff --git a/pkg/storage/unified/search/bleve_search_test.go b/pkg/storage/unified/search/bleve_search_test.go index c1b18724d96..600dd0cfe04 100644 --- a/pkg/storage/unified/search/bleve_search_test.go +++ b/pkg/storage/unified/search/bleve_search_test.go @@ -43,7 +43,7 @@ func indexDocumentsWithTitles(t *testing.T, index resource.ResourceIndex, key re } func checkSearchQuery(t *testing.T, index resource.ResourceIndex, query *resourcepb.ResourceSearchRequest, orderedExpectedNames []string) { - res, err := index.Search(context.Background(), nil, query, nil) + res, err := index.Search(context.Background(), nil, query, nil, nil) require.NoError(t, err) require.Equal(t, int64(len(orderedExpectedNames)), res.TotalHits) for ix, name := range orderedExpectedNames { diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 9bc867ba0d3..b303d11c6e8 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -213,7 +213,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Limit: 100, }, }, - }, nil) + }, nil, nil) require.NoError(t, err) require.Nil(t, rsp.Error) require.NotNil(t, rsp.Results) @@ -242,10 +242,10 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { ] }`, string(disp)) - count, _ := index.DocCount(ctx, "") + count, _ := index.DocCount(ctx, "", nil) assert.Equal(t, int64(3), count) - count, _ = index.DocCount(ctx, "zzz") + count, _ = index.DocCount(ctx, "zzz", nil) assert.Equal(t, int64(1), count) rsp, err = index.Search(ctx, NewStubAccessClient(map[string]bool{"dashboards": true}), &resourcepb.ResourceSearchRequest{ @@ -258,7 +258,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { }}, }, Limit: 100000, - }, nil) + }, nil, nil) require.NoError(t, err) require.Equal(t, int64(2), rsp.TotalHits) require.Equal(t, []string{"aaa", "bbb"}, []string{ @@ -276,7 +276,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { SortBy: []*resourcepb.ResourceSearchRequest_Sort{ {Field: "fields." + DASHBOARD_VIEWS_LAST_1_DAYS, Desc: true}, }, - }, nil) + }, nil, nil) require.NoError(t, err) require.Equal(t, 2, len(rsp.Results.Columns)) require.Equal(t, DASHBOARD_ERRORS_TODAY, rsp.Results.Columns[0].Name) @@ -296,7 +296,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { SortBy: []*resourcepb.ResourceSearchRequest_Sort{ {Field: "fields." + DASHBOARD_VIEWS_LAST_1_DAYS, Desc: true}, }, - }, nil) + }, nil, nil) require.NoError(t, err) require.Equal(t, 0, len(rsp.Results.Rows)) @@ -304,7 +304,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { found, err := index.ListManagedObjects(ctx, &resourcepb.ListManagedObjectsRequest{ Kind: "repo", Id: "repo-1", - }) + }, nil) require.NoError(t, err) jj, err := json.MarshalIndent(found, "", " ") require.NoError(t, err) @@ -341,7 +341,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { ] }`, string(jj)) - counts, err := index.CountManagedObjects(ctx) + counts, err := index.CountManagedObjects(ctx, nil) require.NoError(t, err) jj, err = json.MarshalIndent(counts, "", " ") require.NoError(t, err) @@ -433,7 +433,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Key: key, }, Limit: 100000, - }, nil) + }, nil, nil) require.NoError(t, err) require.Nil(t, rsp.Error) require.NotNil(t, rsp.Results) @@ -468,7 +468,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Limit: 100, }, }, - }, []resource.ResourceIndex{foldersIndex}) // << note the folder index matches the federation request + }, []resource.ResourceIndex{foldersIndex}, nil) // << note the folder index matches the federation request require.NoError(t, err) require.Nil(t, rsp.Error) require.NotNil(t, rsp.Results) @@ -532,7 +532,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Limit: 100, }, }, - }, []resource.ResourceIndex{foldersIndex}) // << note the folder index matches the federation request + }, []resource.ResourceIndex{foldersIndex}, nil) // << note the folder index matches the federation request require.NoError(t, err) require.Equal(t, 3, len(rsp.Results.Rows)) @@ -561,7 +561,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Limit: 100, }, }, - }, []resource.ResourceIndex{foldersIndex}) // << note the folder index matches the federation request + }, []resource.ResourceIndex{foldersIndex}, nil) // << note the folder index matches the federation request require.NoError(t, err) require.Equal(t, 2, len(rsp.Results.Rows)) @@ -589,7 +589,7 @@ func testBleveBackend(t *testing.T, backend *bleveBackend) { Limit: 100, }, }, - }, []resource.ResourceIndex{foldersIndex}) // << note the folder index matches the federation request + }, []resource.ResourceIndex{foldersIndex}, nil) // << note the folder index matches the federation request require.NoError(t, err) require.Equal(t, 0, len(rsp.Results.Rows)) @@ -888,7 +888,7 @@ func TestBuildIndexExpiration(t *testing.T) { idx := backend.GetIndex(ns) require.Nil(t, idx) - _, err = builtIndex.DocCount(context.Background(), "") + _, err = builtIndex.DocCount(context.Background(), "", nil) require.ErrorIs(t, err, bleve.ErrorIndexClosed) // Verify that there are no open indexes. @@ -897,7 +897,7 @@ func TestBuildIndexExpiration(t *testing.T) { idx := backend.GetIndex(ns) require.NotNil(t, idx) - cnt, err := builtIndex.DocCount(context.Background(), "") + cnt, err := builtIndex.DocCount(context.Background(), "", nil) require.NoError(t, err) require.Equal(t, int64(1), cnt) @@ -971,7 +971,7 @@ func TestBuildIndex(t *testing.T) { idx, err := newBackend.BuildIndex(context.Background(), ns, secondIndexDocsCount, nil, "test", indexTestDocs(ns, secondIndexDocsCount, 100), nil, rebuild) require.NoError(t, err) - cnt, err := idx.DocCount(context.Background(), "") + cnt, err := idx.DocCount(context.Background(), "", nil) require.NoError(t, err) if rebuild { require.Equal(t, int64(secondIndexDocsCount), cnt, "Index has been not rebuilt") @@ -1033,10 +1033,10 @@ func TestRebuildingIndexClosesPreviousCachedIndex(t *testing.T) { // Verify that first and second index are different, and first one is now closed. require.NotEqual(t, firstIndex, secondIndex) - _, err = firstIndex.DocCount(context.Background(), "") + _, err = firstIndex.DocCount(context.Background(), "", nil) require.ErrorIs(t, err, bleve.ErrorIndexClosed) - cnt, err := secondIndex.DocCount(context.Background(), "") + cnt, err := secondIndex.DocCount(context.Background(), "", nil) require.NoError(t, err) require.Equal(t, int64(secondSize), cnt) @@ -1341,7 +1341,7 @@ func TestConcurrentIndexUpdateSearchAndRebuild(t *testing.T) { Fields: []string{"title"}, Query: "Document", Limit: 10, - }, nil) + }, nil, nil) if err != nil { if errors.Is(err, bleve.ErrorIndexClosed) || errors.Is(err, context.Canceled) { continue @@ -1574,13 +1574,13 @@ func searchTitle(t *testing.T, idx resource.ResourceIndex, query string, limit i Fields: []string{"title"}, Query: query, Limit: int64(limit), - }, nil) + }, nil, nil) require.NoError(t, err) return resp } func docCount(t *testing.T, idx resource.ResourceIndex) int { - cnt, err := idx.DocCount(context.Background(), "") + cnt, err := idx.DocCount(context.Background(), "", nil) require.NoError(t, err) return int(cnt) } diff --git a/pkg/storage/unified/search/user_test.go b/pkg/storage/unified/search/user_test.go index a58516842c6..67b6777f895 100644 --- a/pkg/storage/unified/search/user_test.go +++ b/pkg/storage/unified/search/user_test.go @@ -7,6 +7,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/selection" + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/tracing" @@ -14,8 +17,6 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/search" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/selection" ) func TestUserDocumentBuilder(t *testing.T) { @@ -175,7 +176,7 @@ func indexUserDocuments(t *testing.T, index resource.ResourceIndex, key resource func checkUserSearchQuery(t *testing.T, index resource.ResourceIndex, query *resourcepb.ResourceSearchRequest, orderedExpectedNames []string) { t.Helper() - res, err := index.Search(context.Background(), nil, query, nil) + res, err := index.Search(context.Background(), nil, query, nil, nil) require.NoError(t, err) require.Equal(t, int64(len(orderedExpectedNames)), res.TotalHits) names := make([]string, len(res.Results.Rows)) diff --git a/pkg/storage/unified/testing/search_backend.go b/pkg/storage/unified/testing/search_backend.go index 4ef4b6d2753..0b3c5f97d25 100644 --- a/pkg/storage/unified/testing/search_backend.go +++ b/pkg/storage/unified/testing/search_backend.go @@ -170,7 +170,7 @@ func runTestResourceIndex(t *testing.T, backend resource.SearchBackend, nsPrefix }, Fields: []string{"title", "folder", "tags"}, Limit: 10, - }, nil) + }, nil, nil) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, int64(1), resp.TotalHits) // Only doc2 should have tag3 now @@ -187,7 +187,7 @@ func runTestResourceIndex(t *testing.T, backend resource.SearchBackend, nsPrefix Query: "Document", Fields: []string{"title", "folder", "tags"}, Limit: 10, - }, nil) + }, nil, nil) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, int64(2), resp.TotalHits) // Both doc1 and doc2 should have doc now @@ -229,7 +229,7 @@ func runTestResourceIndex(t *testing.T, backend resource.SearchBackend, nsPrefix Query: "Document", Fields: []string{"title", "folder", "tags"}, Limit: 10, - }, nil) + }, nil, nil) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, int64(3), resp.TotalHits) // Both doc1, doc2, and doc3 should have doc now @@ -319,7 +319,7 @@ func runTestResourceIndex(t *testing.T, backend resource.SearchBackend, nsPrefix Query: "", Fields: []string{"title"}, Limit: 10, - }, nil) + }, nil, nil) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, int64(1), resp.TotalHits) // Only dash1 should have lib-panel-1 diff --git a/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go b/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go index cdd2b51df00..427131bb90c 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_alertmanager_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/services/featuremgmt" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 41acf2819dc..20484a38bf4 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -20,7 +20,7 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/expr" diff --git a/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json b/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json index 59324e97edd..7bf5ff10535 100644 --- a/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json +++ b/pkg/tests/api/alerting/test-data/alert-notifiers-v1-snapshot.json @@ -392,6 +392,42 @@ "secure": false, "dependsOn": "", "subformOptions": null + }, + { + "element": "checkbox", + "inputType": "", + "label": "HideOpenButton", + "description": "Hide open URL button", + "placeholder": "", + "propertyName": "hide_open_button", + "selectOptions": null, + "showWhen": { + "field": "", + "is": "" + }, + "required": false, + "validationRule": "", + "secure": false, + "dependsOn": "", + "subformOptions": null + }, + { + "element": "checkbox", + "inputType": "", + "label": "HideVersionInfo", + "description": "Hide version info", + "placeholder": "", + "propertyName": "hide_version_info", + "selectOptions": null, + "showWhen": { + "field": "", + "is": "" + }, + "required": false, + "validationRule": "", + "secure": false, + "dependsOn": "", + "subformOptions": null } ] }, diff --git a/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json b/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json index cd5ed8b8f96..d942144ef9c 100644 --- a/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json +++ b/pkg/tests/api/alerting/test-data/alert-notifiers-v2-snapshot.json @@ -1340,6 +1340,42 @@ "secure": false, "dependsOn": "", "subformOptions": null + }, + { + "element": "checkbox", + "inputType": "", + "label": "HideOpenButton", + "description": "Hide open URL button", + "placeholder": "", + "propertyName": "hide_open_button", + "selectOptions": null, + "showWhen": { + "field": "", + "is": "" + }, + "required": false, + "validationRule": "", + "secure": false, + "dependsOn": "", + "subformOptions": null + }, + { + "element": "checkbox", + "inputType": "", + "label": "HideVersionInfo", + "description": "Hide version info", + "placeholder": "", + "propertyName": "hide_version_info", + "selectOptions": null, + "showWhen": { + "field": "", + "is": "" + }, + "required": false, + "validationRule": "", + "secure": false, + "dependsOn": "", + "subformOptions": null } ] } diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index ab33cd9ddbd..b49e61d3a4e 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -17,7 +17,7 @@ import ( "github.com/prometheus/common/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/configprovider" diff --git a/pkg/tests/apis/preferences/stars_test.go b/pkg/tests/apis/collections/stars_test.go similarity index 78% rename from pkg/tests/apis/preferences/stars_test.go rename to pkg/tests/apis/collections/stars_test.go index f1050593926..fe841fb4851 100644 --- a/pkg/tests/apis/preferences/stars_test.go +++ b/pkg/tests/apis/collections/stars_test.go @@ -1,4 +1,4 @@ -package preferences +package collections import ( "context" @@ -11,17 +11,21 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - preferences "github.com/grafana/grafana/apps/preferences/pkg/apis/preferences/v1alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/setting" "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/testutil" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationStars(t *testing.T) { testutil.SkipIntegrationTestInShortMode(t) @@ -47,10 +51,10 @@ func TestIntegrationStars(t *testing.T) { "folders.folder.grafana.app": { DualWriterMode: mode, }, - "stars.preferences.grafana.app": { + "stars.collections.grafana.app": { DualWriterMode: mode, }, - "preferences.preferences.grafana.app": { + "collections.collections.grafana.app": { DualWriterMode: mode, }, }, @@ -60,28 +64,17 @@ func TestIntegrationStars(t *testing.T) { ctx := context.Background() starsClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, - GVR: preferences.StarsResourceInfo.GroupVersionResource(), + GVR: collections.StarsResourceInfo.GroupVersionResource(), }) starsClientViewer := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Viewer, - GVR: preferences.StarsResourceInfo.GroupVersionResource(), + GVR: collections.StarsResourceInfo.GroupVersionResource(), }) dashboardClient := helper.GetResourceClient(apis.ResourceClientArgs{ User: helper.Org1.Admin, GVR: dashboardV1.DashboardResourceInfo.GroupVersionResource(), }) - history := &queryhistory.QueryHistoryResponse{} - legacyHistoryResponse := apis.DoRequest(helper, apis.RequestParams{ - User: starsClient.Args.User, - Method: http.MethodPost, - Path: "/api/query-history", - Body: []byte(`{"dataSourceUid":"eez1ebbdn3pq8b","queries":[{"scenarioId":"random_walk","seriesCount":1,"refId":"A","datasource":{"type":"grafana-testdata-datasource","uid":"eez1ebbdn3pq8b","apiVersion":"v0alpha1"}}]}`), - }, &history) - require.Equal(t, http.StatusOK, legacyHistoryResponse.Response.StatusCode, "add query history") - queryHistoryStarUID := history.Result.UID - require.NotEmpty(t, queryHistoryStarUID, "expect a query history UID") - // Create 5 dashboards for i := range 5 { _, err := dashboardClient.Resource.Create(context.Background(), &unstructured.Unstructured{ @@ -126,7 +119,7 @@ func TestIntegrationStars(t *testing.T) { // List values and compare results rsp, err = starsClient.Resource.List(ctx, metav1.ListOptions{}) require.NoError(t, err) - stars := typed(t, rsp, &preferences.StarsList{}) + stars := typed(t, rsp, &collections.StarsList{}) require.Len(t, stars.Items, 1, "user stars should exist") require.Equal(t, "user-"+starsClient.Args.User.Identity.GetIdentifier(), @@ -148,7 +141,7 @@ func TestIntegrationStars(t *testing.T) { rspObj, err := starsClient.Resource.Get(ctx, "user-"+starsClient.Args.User.Identity.GetIdentifier(), metav1.GetOptions{}) require.NoError(t, err) - after := typed(t, rspObj, &preferences.Stars{}) + after := typed(t, rspObj, &collections.Stars{}) resources = after.Spec.Resource require.Len(t, resources, 1) require.Equal(t, "dashboard.grafana.app", resources[0].Group) @@ -175,7 +168,7 @@ func TestIntegrationStars(t *testing.T) { }, metav1.UpdateOptions{}) require.NoError(t, err) - after = typed(t, rspObj, &preferences.Stars{}) + after = typed(t, rspObj, &collections.Stars{}) resources = after.Spec.Resource require.Len(t, resources, 1) require.Equal(t, "dashboard.grafana.app", resources[0].Group) @@ -184,19 +177,10 @@ func TestIntegrationStars(t *testing.T) { []string{"aaa", "bbb", "test-2"}, // NOTE 2 stays, 3 removed, added aaa+bbb (and sorted!) resources[0].Names) - // Query history stars - legacyHistoryResponse = apis.DoRequest(helper, apis.RequestParams{ - User: starsClient.Args.User, - Method: http.MethodPost, - Path: "/api/query-history/star/" + queryHistoryStarUID, - }, &history) - require.Equal(t, http.StatusOK, legacyHistoryResponse.Response.StatusCode, "add query history") - require.True(t, history.Result.Starred, "expect the value to be starred") - rspObj, err = starsClient.Resource.Get(ctx, "user-"+starsClient.Args.User.Identity.GetIdentifier(), metav1.GetOptions{}) require.NoError(t, err) - after = typed(t, rspObj, &preferences.Stars{}) + after = typed(t, rspObj, &collections.Stars{}) jj, err := json.MarshalIndent(after.Spec, "", " ") require.NoError(t, err) require.JSONEq(t, `{ @@ -209,13 +193,6 @@ func TestIntegrationStars(t *testing.T) { "bbb", "test-2" ] - }, - { - "group": "history.grafana.app", - "kind": "Query", - "names": [ - "`+queryHistoryStarUID+`" - ] } ] }`, string(jj)) diff --git a/pkg/tests/apis/dashboard/search_test.go b/pkg/tests/apis/dashboard/search_test.go new file mode 100644 index 00000000000..129a44aee21 --- /dev/null +++ b/pkg/tests/apis/dashboard/search_test.go @@ -0,0 +1,286 @@ +package dashboards + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + k8srest "k8s.io/client-go/rest" + + dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/apis" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationSearchPermissionFiltering(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + // Only run for Unified Storage modes that support search (Mode3+) + modes := []rest.DualWriterMode{rest.Mode3, rest.Mode4, rest.Mode5} + for _, mode := range modes { + runSearchPermissionTest(t, mode) + } +} + +func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { + t.Run(fmt.Sprintf("search permission filtering with dual writer mode %d", mode), func(t *testing.T) { + ctx := context.Background() + + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": {DualWriterMode: mode}, + "folders.folder.grafana.app": {DualWriterMode: mode}, + }, + UnifiedStorageEnableSearch: true, + }) + defer helper.Shutdown() + + // Create a folder via legacy API using Admin + folderUID := "perm-test-folder" + { + cfg := dynamic.ConfigFor(helper.Org1.Admin.NewRestConfig()) + cfg.GroupVersion = &schema.GroupVersion{Group: "folder.grafana.app", Version: "v1beta1"} + restClient, err := k8srest.RESTClientFor(cfg) + require.NoError(t, err) + + var statusCode int + body := []byte(fmt.Sprintf(`{"uid":"%s","title":"Permission Test Folder"}`, folderUID)) + result := restClient.Post().AbsPath("api", "folders"). + Body(body). + SetHeader("Content-type", "application/json"). + Do(ctx). + StatusCode(&statusCode) + require.NoError(t, result.Error()) + require.Equal(t, int(http.StatusOK), statusCode) + } + + // Set permissions: Viewer gets View, Editor gets Edit + viewerID, err := identity.UserIdentifier(helper.Org1.Viewer.Identity.GetID()) + require.NoError(t, err) + editorID, err := identity.UserIdentifier(helper.Org1.Editor.Identity.GetID()) + require.NoError(t, err) + + permissions := []ResourcePermissionSetting{ + {UserID: &viewerID, Level: ResourcePermissionLevelView}, + {UserID: &editorID, Level: ResourcePermissionLevelEdit}, + } + setFolderPermissions(t, helper, helper.Org1.Admin, folderUID, permissions) + + // Helper to call search + callSearch := func(user apis.User, params string) dashboardV0.SearchResults { + ns := user.Identity.GetNamespace() + cfg := dynamic.ConfigFor(user.NewRestConfig()) + cfg.GroupVersion = &schema.GroupVersion{Group: "dashboard.grafana.app", Version: "v0alpha1"} + restClient, err := k8srest.RESTClientFor(cfg) + require.NoError(t, err) + + var statusCode int + req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search"). + Param("limit", "1000"). + Param("type", "folder") // Only search folders + + for _, kv := range strings.Split(params, "&") { + if kv == "" { + continue + } + parts := strings.SplitN(kv, "=", 2) + if len(parts) == 2 { + req = req.Param(parts[0], parts[1]) + } + } + res := req.Do(ctx).StatusCode(&statusCode) + require.NoError(t, res.Error()) + require.Equal(t, int(http.StatusOK), statusCode) + var sr dashboardV0.SearchResults + raw, err := res.Raw() + require.NoError(t, err) + require.NoError(t, json.Unmarshal(raw, &sr)) + return sr + } + + // 1. Viewer searching without permission parameter should find it (has View access) + { + res := callSearch(helper.Org1.Viewer, "") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Viewer should find folder without permission parameter") + } + + // 2. Viewer searching with permission=View should find it + { + res := callSearch(helper.Org1.Viewer, "permission=View") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Viewer should find folder with permission=View") + } + + // 3. Viewer searching with permission=Edit should NOT find it + { + res := callSearch(helper.Org1.Viewer, "permission=Edit") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.False(t, found, "Viewer should NOT find folder with permission=Edit") + } + + // 4. Editor searching with permission=Edit should find it + { + res := callSearch(helper.Org1.Editor, "permission=Edit") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Editor should find folder with permission=Edit") + } + + // 5. Editor searching with permission=View should find it (Edit permission includes View) + { + res := callSearch(helper.Org1.Editor, "permission=View") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Editor should find folder with permission=View (Edit includes View)") + } + + // 6. Editor searching without permission parameter should find it (has Edit access) + { + res := callSearch(helper.Org1.Editor, "") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Editor should find folder without permission parameter") + } + + // 7. Admin searching with permission=View should find it (Admin has full access) + { + res := callSearch(helper.Org1.Admin, "permission=View") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Admin should find folder with permission=View") + } + + // 8. Admin searching with permission=Edit should find it (Admin has full access) + { + res := callSearch(helper.Org1.Admin, "permission=Edit") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Admin should find folder with permission=Edit") + } + + // 9. Admin searching with permission=Admin should find it (Admin has full access) + { + res := callSearch(helper.Org1.Admin, "permission=Admin") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Admin should find folder with permission=Admin") + } + + // 10. Admin searching without permission parameter should find it (has Admin access) + { + res := callSearch(helper.Org1.Admin, "") + found := false + for _, h := range res.Hits { + if h.Name == folderUID { // Verify it's our folder + found = true + break + } + } + require.True(t, found, "Admin should find folder without permission parameter") + } + }) +} + +// Types and helpers for permission setting + +type ResourcePermissionLevel int + +const ( + ResourcePermissionLevelView ResourcePermissionLevel = 1 + ResourcePermissionLevelEdit ResourcePermissionLevel = 2 + ResourcePermissionLevelAdmin ResourcePermissionLevel = 4 +) + +type ResourcePermissionSetting struct { + UserID *int64 `json:"userId,omitempty"` + TeamID *int64 `json:"teamId,omitempty"` + Role *string `json:"role,omitempty"` + Level ResourcePermissionLevel `json:"permission"` +} + +type permissionRequest struct { + Items []ResourcePermissionSetting `json:"items"` +} + +func setFolderPermissions(t *testing.T, helper *apis.K8sTestHelper, actingUser apis.User, folderUID string, permissions []ResourcePermissionSetting) { + reqBody := permissionRequest{ + Items: permissions, + } + + jsonBody, err := json.Marshal(reqBody) + require.NoError(t, err, "Failed to marshal permissions to JSON") + + path := fmt.Sprintf("/api/folders/%s/permissions", folderUID) + + resp := apis.DoRequest(helper, apis.RequestParams{ + User: actingUser, + Method: http.MethodPost, + Path: path, + Body: jsonBody, + ContentType: "application/json", + }, &struct{}{}) + + require.Equal(t, http.StatusOK, resp.Response.StatusCode, "Failed to set permissions for folder %s", folderUID) +} diff --git a/pkg/tests/apis/openapi_snapshots/collections.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/collections.grafana.app-v1alpha1.json new file mode 100644 index 00000000000..8f399c50f15 --- /dev/null +++ b/pkg/tests/apis/openapi_snapshots/collections.grafana.app-v1alpha1.json @@ -0,0 +1,1615 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana collections", + "title": "collections.grafana.app/v1alpha1" + }, + "paths": { + "/apis/collections.grafana.app/v1alpha1/": { + "get": { + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/collections.grafana.app/v1alpha1/namespaces/{namespace}/stars": { + "get": { + "tags": [ + "Stars" + ], + "description": "list objects of kind Stars", + "operationId": "listStars", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "post": { + "tags": [ + "Stars" + ], + "description": "create Stars", + "operationId": "createStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "delete": { + "tags": [ + "Stars" + ], + "description": "delete collection of Stars", + "operationId": "deletecollectionStars", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/collections.grafana.app/v1alpha1/namespaces/{namespace}/stars/{name}": { + "get": { + "tags": [ + "Stars" + ], + "description": "read the specified Stars", + "operationId": "getStars", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "put": { + "tags": [ + "Stars" + ], + "description": "replace the specified Stars", + "operationId": "replaceStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "delete": { + "tags": [ + "Stars" + ], + "description": "delete Stars", + "operationId": "deleteStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "patch": { + "tags": [ + "Stars" + ], + "description": "partially update the specified Stars", + "operationId": "updateStars", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Stars", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/collections.grafana.app/v1alpha1/namespaces/{namespace}/stars/{name}/update/{group}/{kind}/{id}": { + "put": { + "tags": [ + "Stars" + ], + "description": "Add a starred item", + "operationId": "addStar", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "delete": { + "tags": [ + "Stars" + ], + "description": "Remove a starred item", + "operationId": "removeStar", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "collections.grafana.app", + "version": "v1alpha1", + "kind": "Stars" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Stars", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "group", + "in": "path", + "description": "API group for stared item", + "required": true, + "schema": { + "type": "string" + }, + "example": "dashboard.grafana.app" + }, + { + "name": "kind", + "in": "path", + "description": "Kind for stared item", + "required": true, + "schema": { + "type": "string" + }, + "example": "Dashboard" + }, + { + "name": "id", + "in": "path", + "description": "The k8s name for the selected item", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars": { + "type": "object", + "required": [ + "metadata", + "spec" + ], + "properties": { + "apiVersion": { + "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" + }, + "kind": { + "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" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the Stars", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsSpec" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "collections.grafana.app", + "kind": "Stars", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "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" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.Stars" + } + ] + } + }, + "kind": { + "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" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "collections.grafana.app", + "kind": "StarsList", + "version": "v1alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsResource": { + "type": "object", + "required": [ + "group", + "kind", + "names" + ], + "properties": { + "group": { + "type": "string", + "default": "" + }, + "kind": { + "type": "string", + "default": "" + }, + "names": { + "description": "The set of resources", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set" + } + } + }, + "com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsSpec": { + "type": "object", + "required": [ + "resource" + ], + "properties": { + "resource": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.collections.pkg.apis.collections.v1alpha1.StarsResource" + } + ] + } + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": [ + "groupVersion", + "resources" + ], + "properties": { + "apiVersion": { + "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" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "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" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "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" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "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" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "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" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "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" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + } + } + } +} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index 42af6d54a98..06bad0ba004 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1820,6 +1820,19 @@ "type": "string" } }, + { + "name": "permission", + "in": "query", + "description": "permission needed for the resource (View, Edit, Admin)", + "schema": { + "type": "string", + "enum": [ + "View", + "Edit", + "Admin" + ] + } + }, { "name": "sort", "in": "query", diff --git a/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json b/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json index 1b9b680af34..a2a645aabf2 100644 --- a/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/preferences.grafana.app-v1alpha1.json @@ -1,7 +1,6 @@ { "openapi": "3.0.0", "info": { - "description": "Grafana preferences", "title": "preferences.grafana.app/v1alpha1" }, "paths": { @@ -741,939 +740,6 @@ } } ] - }, - "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/stars": { - "get": { - "tags": [ - "Stars" - ], - "description": "list objects of kind Stars", - "operationId": "listStars", - "parameters": [ - { - "name": "allowWatchBookmarks", - "in": "query", - "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "watch", - "in": "query", - "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" - } - }, - "application/json;stream=watch": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" - } - }, - "application/vnd.kubernetes.protobuf;stream=watch": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList" - } - } - } - } - }, - "x-kubernetes-action": "list", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "post": { - "tags": [ - "Stars" - ], - "description": "create Stars", - "operationId": "createStars", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - }, - "202": { - "description": "Accepted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - } - }, - "x-kubernetes-action": "post", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "delete": { - "tags": [ - "Stars" - ], - "description": "delete collection of Stars", - "operationId": "deletecollectionStars", - "parameters": [ - { - "name": "continue", - "in": "query", - "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "gracePeriodSeconds", - "in": "query", - "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "ignoreStoreReadErrorWithClusterBreakingPotential", - "in": "query", - "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "labelSelector", - "in": "query", - "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "limit", - "in": "query", - "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "orphanDependents", - "in": "query", - "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "propagationPolicy", - "in": "query", - "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersion", - "in": "query", - "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "resourceVersionMatch", - "in": "query", - "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "sendInitialEvents", - "in": "query", - "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "timeoutSeconds", - "in": "query", - "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", - "schema": { - "type": "integer", - "uniqueItems": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - } - } - } - }, - "x-kubernetes-action": "deletecollection", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "parameters": [ - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, - "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/stars/{name}": { - "get": { - "tags": [ - "Stars" - ], - "description": "read the specified Stars", - "operationId": "getStars", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - } - }, - "x-kubernetes-action": "get", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "put": { - "tags": [ - "Stars" - ], - "description": "replace the specified Stars", - "operationId": "replaceStars", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - } - }, - "x-kubernetes-action": "put", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "delete": { - "tags": [ - "Stars" - ], - "description": "delete Stars", - "operationId": "deleteStars", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "gracePeriodSeconds", - "in": "query", - "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", - "schema": { - "type": "integer", - "uniqueItems": true - } - }, - { - "name": "ignoreStoreReadErrorWithClusterBreakingPotential", - "in": "query", - "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "orphanDependents", - "in": "query", - "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - }, - { - "name": "propagationPolicy", - "in": "query", - "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - } - } - }, - "202": { - "description": "Accepted", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" - } - } - } - } - }, - "x-kubernetes-action": "delete", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "patch": { - "tags": [ - "Stars" - ], - "description": "partially update the specified Stars", - "operationId": "updateStars", - "parameters": [ - { - "name": "dryRun", - "in": "query", - "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldManager", - "in": "query", - "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "fieldValidation", - "in": "query", - "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "force", - "in": "query", - "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", - "schema": { - "type": "boolean", - "uniqueItems": true - } - } - ], - "requestBody": { - "content": { - "application/apply-patch+yaml": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/json-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - }, - "application/strategic-merge-patch+json": { - "schema": { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - }, - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/vnd.kubernetes.protobuf": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - }, - "application/yaml": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - } - }, - "x-kubernetes-action": "patch", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the Stars", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "pretty", - "in": "query", - "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", - "schema": { - "type": "string", - "uniqueItems": true - } - } - ] - }, - "/apis/preferences.grafana.app/v1alpha1/namespaces/{namespace}/stars/{name}/update/{group}/{kind}/{id}": { - "put": { - "tags": [ - "Stars" - ], - "description": "Add a starred item", - "operationId": "addStar", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - } - }, - "x-kubernetes-action": "connect", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "delete": { - "tags": [ - "Stars" - ], - "description": "Remove a starred item", - "operationId": "removeStar", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - } - } - } - }, - "x-kubernetes-action": "connect", - "x-kubernetes-group-version-kind": { - "group": "preferences.grafana.app", - "version": "v1alpha1", - "kind": "Stars" - } - }, - "parameters": [ - { - "name": "name", - "in": "path", - "description": "name of the Stars", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "namespace", - "in": "path", - "description": "object name and auth scope, such as for teams and projects", - "required": true, - "schema": { - "type": "string", - "uniqueItems": true - } - }, - { - "name": "group", - "in": "path", - "description": "API group for stared item", - "required": true, - "schema": { - "type": "string" - }, - "example": "dashboard.grafana.app" - }, - { - "name": "kind", - "in": "path", - "description": "Kind for stared item", - "required": true, - "schema": { - "type": "string" - }, - "example": "Dashboard" - }, - { - "name": "id", - "in": "path", - "description": "The k8s name for the selected item", - "required": true, - "schema": { - "type": "string" - } - } - ] } }, "components": { @@ -1853,136 +919,6 @@ } } }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars": { - "type": "object", - "required": [ - "metadata", - "spec" - ], - "properties": { - "apiVersion": { - "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" - }, - "kind": { - "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" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" - } - ] - }, - "spec": { - "description": "Spec is the spec of the Stars", - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsSpec" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "preferences.grafana.app", - "kind": "Stars", - "version": "v1alpha1" - } - ] - }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsList": { - "type": "object", - "required": [ - "metadata", - "items" - ], - "properties": { - "apiVersion": { - "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" - }, - "items": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.Stars" - } - ] - } - }, - "kind": { - "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" - }, - "metadata": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" - } - ] - } - }, - "x-kubernetes-group-version-kind": [ - { - "group": "preferences.grafana.app", - "kind": "StarsList", - "version": "v1alpha1" - } - ] - }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsResource": { - "type": "object", - "required": [ - "group", - "kind", - "names" - ], - "properties": { - "group": { - "type": "string", - "default": "" - }, - "kind": { - "type": "string", - "default": "" - }, - "names": { - "description": "The set of resources", - "type": "array", - "items": { - "type": "string", - "default": "" - }, - "x-kubernetes-list-type": "set" - } - } - }, - "com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsSpec": { - "type": "object", - "required": [ - "resource" - ], - "properties": { - "resource": { - "type": "array", - "items": { - "default": {}, - "allOf": [ - { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.preferences.pkg.apis.preferences.v1alpha1.StarsResource" - } - ] - } - } - } - }, "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { "description": "APIResource specifies the name of a resource and whether it is namespaced.", "type": "object", diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index b218095d938..3df0b3db0ff 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -97,6 +97,9 @@ func TestIntegrationOpenAPIs(t *testing.T) { }, { Group: "preferences.grafana.app", Version: "v1alpha1", + }, { + Group: "collections.grafana.app", + Version: "v1alpha1", }, { Group: "notifications.alerting.grafana.app", Version: "v0alpha1", diff --git a/pkg/tsdb/cloudwatch/log_query.go b/pkg/tsdb/cloudwatch/log_query.go index 43237bece10..9284581b213 100644 --- a/pkg/tsdb/cloudwatch/log_query.go +++ b/pkg/tsdb/cloudwatch/log_query.go @@ -109,7 +109,8 @@ func logsResultsToDataframes(response *cloudwatchlogs.GetQueryResultsOutput, gro newFields[len(newFields)-1].SetConfig( &data.FieldConfig{ Custom: map[string]any{ - "hidden": true, + "hidden": true, + "hideFrom": map[string]any{"viz": true}, }, }, ) diff --git a/pkg/tsdb/cloudwatch/log_query_test.go b/pkg/tsdb/cloudwatch/log_query_test.go index 9e86b4e752c..c694eb15df9 100644 --- a/pkg/tsdb/cloudwatch/log_query_test.go +++ b/pkg/tsdb/cloudwatch/log_query_test.go @@ -169,7 +169,8 @@ func TestLogsResultsToDataframes(t *testing.T) { }) hiddenLogStreamField.SetConfig(&data.FieldConfig{ Custom: map[string]any{ - "hidden": true, + "hidden": true, + "hideFrom": map[string]any{"viz": true}, }, }) @@ -180,7 +181,8 @@ func TestLogsResultsToDataframes(t *testing.T) { }) hiddenLogField.SetConfig(&data.FieldConfig{ Custom: map[string]any{ - "hidden": true, + "hidden": true, + "hideFrom": map[string]any{"viz": true}, }, }) diff --git a/pkg/tsdb/elasticsearch/aggregation_factory.go b/pkg/tsdb/elasticsearch/aggregation_factory.go new file mode 100644 index 00000000000..cc3e597e50b --- /dev/null +++ b/pkg/tsdb/elasticsearch/aggregation_factory.go @@ -0,0 +1,173 @@ +package elasticsearch + +import ( + "regexp" + + "github.com/grafana/grafana/pkg/components/simplejson" + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" +) + +// addDateHistogramAgg adds a date histogram aggregation to the aggregation builder +func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFrom, timeTo int64, timeField string) es.AggBuilder { + // If no field is specified, use the time field + field := bucketAgg.Field + if field == "" { + field = timeField + } + aggBuilder.DateHistogram(bucketAgg.ID, field, func(a *es.DateHistogramAgg, b es.AggBuilder) { + var interval = bucketAgg.Settings.Get("interval").MustString("auto") + if isCalendarInterval(interval) { + a.CalendarInterval = interval + } else { + if interval == "auto" { + // note this is not really a valid grafana-variable-handling, + // because normally this would not match `$__interval_ms`, + // but because how we apply these in the go-code, this will work + // correctly, and becomes something like `500ms`. + // a nicer way would be to use `${__interval_ms}ms`, but + // that format is not recognized where we apply these variables + // in the elasticsearch datasource + a.FixedInterval = "$__interval_msms" + } else { + a.FixedInterval = interval + } + } + a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0) + a.ExtendedBounds = &es.ExtendedBounds{Min: timeFrom, Max: timeTo} + a.Format = bucketAgg.Settings.Get("format").MustString(es.DateFormatEpochMS) + + if offset, err := bucketAgg.Settings.Get("offset").String(); err == nil { + a.Offset = offset + } + + if missing, err := bucketAgg.Settings.Get("missing").String(); err == nil { + a.Missing = &missing + } + + if timezone, err := bucketAgg.Settings.Get("timeZone").String(); err == nil { + if timezone != "utc" { + a.TimeZone = timezone + } + } + + aggBuilder = b + }) + + return aggBuilder +} + +// addHistogramAgg adds a histogram aggregation to the aggregation builder +func addHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { + aggBuilder.Histogram(bucketAgg.ID, bucketAgg.Field, func(a *es.HistogramAgg, b es.AggBuilder) { + a.Interval = stringToFloatWithDefaultValue(bucketAgg.Settings.Get("interval").MustString(), 1000) + a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0) + + if missing, err := bucketAgg.Settings.Get("missing").Int(); err == nil { + a.Missing = &missing + } + + aggBuilder = b + }) + + return aggBuilder +} + +// addTermsAgg adds a terms aggregation to the aggregation builder +func addTermsAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, metrics []*MetricAgg) es.AggBuilder { + aggBuilder.Terms(bucketAgg.ID, bucketAgg.Field, func(a *es.TermsAggregation, b es.AggBuilder) { + if size, err := bucketAgg.Settings.Get("size").Int(); err == nil { + a.Size = size + } else { + a.Size = stringToIntWithDefaultValue(bucketAgg.Settings.Get("size").MustString(), defaultSize) + } + + if minDocCount, err := bucketAgg.Settings.Get("min_doc_count").Int(); err == nil { + a.MinDocCount = &minDocCount + } + if missing, err := bucketAgg.Settings.Get("missing").String(); err == nil { + a.Missing = &missing + } + + if orderBy, err := bucketAgg.Settings.Get("orderBy").String(); err == nil { + /* + The format for extended stats and percentiles is {metricId}[bucket_path] + for everything else it's just {metricId}, _count, _term, or _key + */ + metricIdRegex := regexp.MustCompile(`^(\d+)`) + metricId := metricIdRegex.FindString(orderBy) + + if len(metricId) > 0 { + for _, m := range metrics { + if m.ID == metricId { + if m.Type == "count" { + a.Order["_count"] = bucketAgg.Settings.Get("order").MustString("desc") + } else { + a.Order[orderBy] = bucketAgg.Settings.Get("order").MustString("desc") + b.Metric(m.ID, m.Type, m.Field, nil) + } + break + } + } + } else { + a.Order[orderBy] = bucketAgg.Settings.Get("order").MustString("desc") + } + } + + aggBuilder = b + }) + + return aggBuilder +} + +// addNestedAgg adds a nested aggregation to the aggregation builder +func addNestedAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { + aggBuilder.Nested(bucketAgg.ID, bucketAgg.Field, func(a *es.NestedAggregation, b es.AggBuilder) { + aggBuilder = b + }) + + return aggBuilder +} + +// addFiltersAgg adds a filters aggregation to the aggregation builder +func addFiltersAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { + filters := make(map[string]any) + for _, filter := range bucketAgg.Settings.Get("filters").MustArray() { + json := simplejson.NewFromAny(filter) + query := json.Get("query").MustString() + label := json.Get("label").MustString() + if label == "" { + label = query + } + filters[label] = &es.QueryStringFilter{Query: query, AnalyzeWildcard: true} + } + + if len(filters) > 0 { + aggBuilder.Filters(bucketAgg.ID, func(a *es.FiltersAggregation, b es.AggBuilder) { + a.Filters = filters + aggBuilder = b + }) + } + + return aggBuilder +} + +// addGeoHashGridAgg adds a geohash grid aggregation to the aggregation builder +func addGeoHashGridAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { + aggBuilder.GeoHashGrid(bucketAgg.ID, bucketAgg.Field, func(a *es.GeoHashGridAggregation, b es.AggBuilder) { + a.Precision = stringToIntWithDefaultValue(bucketAgg.Settings.Get("precision").MustString(), es.DefaultGeoHashPrecision) + aggBuilder = b + }) + + return aggBuilder +} + +// isCalendarInterval checks if the interval is a calendar interval +func isCalendarInterval(interval string) bool { + calendarIntervals := []string{"1w", "1M", "1q", "1y"} + for _, ci := range calendarIntervals { + if interval == ci { + return true + } + } + return false +} diff --git a/pkg/tsdb/elasticsearch/data_query.go b/pkg/tsdb/elasticsearch/data_query.go index 53a776e97df..e883b3d769c 100644 --- a/pkg/tsdb/elasticsearch/data_query.go +++ b/pkg/tsdb/elasticsearch/data_query.go @@ -6,16 +6,12 @@ import ( "errors" "fmt" "net/url" - "regexp" - "slices" - "strconv" "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana/pkg/components/simplejson" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) @@ -108,436 +104,3 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) { return parseResponse(e.ctx, res.Responses, queries, e.client.GetConfiguredFields(), e.keepLabelsInResponse, e.logger) } - -func (e *elasticsearchDataQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilder, from, to int64) error { - err := isQueryWithError(q) - if err != nil { - return backend.DownstreamError(fmt.Errorf("received invalid query. %w", err)) - } - - defaultTimeField := e.client.GetConfiguredFields().TimeField - b := ms.Search(q.Interval, q.TimeRange) - b.Size(0) - filters := b.Query().Bool().Filter() - filters.AddDateRangeFilter(defaultTimeField, to, from, es.DateFormatEpochMS) - filters.AddQueryStringFilter(q.RawQuery, true) - - if isLogsQuery(q) { - processLogsQuery(q, b, from, to, defaultTimeField) - } else if isDocumentQuery(q) { - processDocumentQuery(q, b, from, to, defaultTimeField) - } else { - // Otherwise, it is a time series query and we process it - processTimeSeriesQuery(q, b, from, to, defaultTimeField) - } - - return nil -} - -func setFloatPath(settings *simplejson.Json, path ...string) { - if stringValue, err := settings.GetPath(path...).String(); err == nil { - if value, err := strconv.ParseFloat(stringValue, 64); err == nil { - settings.SetPath(path, value) - } - } -} - -func setIntPath(settings *simplejson.Json, path ...string) { - if stringValue, err := settings.GetPath(path...).String(); err == nil { - if value, err := strconv.ParseInt(stringValue, 10, 64); err == nil { - settings.SetPath(path, value) - } - } -} - -// Casts values to float when required by Elastic's query DSL -func (metricAggregation MetricAgg) generateSettingsForDSL() map[string]any { - switch metricAggregation.Type { - case "moving_avg": - setFloatPath(metricAggregation.Settings, "window") - setFloatPath(metricAggregation.Settings, "predict") - setFloatPath(metricAggregation.Settings, "settings", "alpha") - setFloatPath(metricAggregation.Settings, "settings", "beta") - setFloatPath(metricAggregation.Settings, "settings", "gamma") - setFloatPath(metricAggregation.Settings, "settings", "period") - case "serial_diff": - setFloatPath(metricAggregation.Settings, "lag") - } - - if isMetricAggregationWithInlineScriptSupport(metricAggregation.Type) { - scriptValue, err := metricAggregation.Settings.GetPath("script").String() - if err != nil { - // the script is stored using the old format : `script:{inline: "value"}` or is not set - scriptValue, err = metricAggregation.Settings.GetPath("script", "inline").String() - } - - if err == nil { - metricAggregation.Settings.SetPath([]string{"script"}, scriptValue) - } - } - - return metricAggregation.Settings.MustMap() -} - -func (bucketAgg BucketAgg) generateSettingsForDSL() map[string]any { - setIntPath(bucketAgg.Settings, "min_doc_count") - - return bucketAgg.Settings.MustMap() -} - -func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFrom, timeTo int64, timeField string) es.AggBuilder { - // If no field is specified, use the time field - field := bucketAgg.Field - if field == "" { - field = timeField - } - aggBuilder.DateHistogram(bucketAgg.ID, field, func(a *es.DateHistogramAgg, b es.AggBuilder) { - var interval = bucketAgg.Settings.Get("interval").MustString("auto") - if slices.Contains(es.GetCalendarIntervals(), interval) { - a.CalendarInterval = interval - } else { - if interval == "auto" { - // note this is not really a valid grafana-variable-handling, - // because normally this would not match `$__interval_ms`, - // but because how we apply these in the go-code, this will work - // correctly, and becomes something like `500ms`. - // a nicer way would be to use `${__interval_ms}ms`, but - // that format is not recognized where we apply these variables - // in the elasticsearch datasource - a.FixedInterval = "$__interval_msms" - } else { - a.FixedInterval = interval - } - } - a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0) - a.ExtendedBounds = &es.ExtendedBounds{Min: timeFrom, Max: timeTo} - a.Format = bucketAgg.Settings.Get("format").MustString(es.DateFormatEpochMS) - - if offset, err := bucketAgg.Settings.Get("offset").String(); err == nil { - a.Offset = offset - } - - if missing, err := bucketAgg.Settings.Get("missing").String(); err == nil { - a.Missing = &missing - } - - if timezone, err := bucketAgg.Settings.Get("timeZone").String(); err == nil { - if timezone != "utc" { - a.TimeZone = timezone - } - } - - aggBuilder = b - }) - - return aggBuilder -} - -func addHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { - aggBuilder.Histogram(bucketAgg.ID, bucketAgg.Field, func(a *es.HistogramAgg, b es.AggBuilder) { - a.Interval = stringToFloatWithDefaultValue(bucketAgg.Settings.Get("interval").MustString(), 1000) - a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0) - - if missing, err := bucketAgg.Settings.Get("missing").Int(); err == nil { - a.Missing = &missing - } - - aggBuilder = b - }) - - return aggBuilder -} - -func addTermsAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, metrics []*MetricAgg) es.AggBuilder { - aggBuilder.Terms(bucketAgg.ID, bucketAgg.Field, func(a *es.TermsAggregation, b es.AggBuilder) { - if size, err := bucketAgg.Settings.Get("size").Int(); err == nil { - a.Size = size - } else { - a.Size = stringToIntWithDefaultValue(bucketAgg.Settings.Get("size").MustString(), defaultSize) - } - - if minDocCount, err := bucketAgg.Settings.Get("min_doc_count").Int(); err == nil { - a.MinDocCount = &minDocCount - } - if missing, err := bucketAgg.Settings.Get("missing").String(); err == nil { - a.Missing = &missing - } - - if orderBy, err := bucketAgg.Settings.Get("orderBy").String(); err == nil { - /* - The format for extended stats and percentiles is {metricId}[bucket_path] - for everything else it's just {metricId}, _count, _term, or _key - */ - metricIdRegex := regexp.MustCompile(`^(\d+)`) - metricId := metricIdRegex.FindString(orderBy) - - if len(metricId) > 0 { - for _, m := range metrics { - if m.ID == metricId { - if m.Type == "count" { - a.Order["_count"] = bucketAgg.Settings.Get("order").MustString("desc") - } else { - a.Order[orderBy] = bucketAgg.Settings.Get("order").MustString("desc") - b.Metric(m.ID, m.Type, m.Field, nil) - } - break - } - } - } else { - a.Order[orderBy] = bucketAgg.Settings.Get("order").MustString("desc") - } - } - - aggBuilder = b - }) - - return aggBuilder -} - -func addNestedAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { - aggBuilder.Nested(bucketAgg.ID, bucketAgg.Field, func(a *es.NestedAggregation, b es.AggBuilder) { - aggBuilder = b - }) - - return aggBuilder -} - -func addFiltersAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { - filters := make(map[string]any) - for _, filter := range bucketAgg.Settings.Get("filters").MustArray() { - json := simplejson.NewFromAny(filter) - query := json.Get("query").MustString() - label := json.Get("label").MustString() - if label == "" { - label = query - } - filters[label] = &es.QueryStringFilter{Query: query, AnalyzeWildcard: true} - } - - if len(filters) > 0 { - aggBuilder.Filters(bucketAgg.ID, func(a *es.FiltersAggregation, b es.AggBuilder) { - a.Filters = filters - aggBuilder = b - }) - } - - return aggBuilder -} - -func addGeoHashGridAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg) es.AggBuilder { - aggBuilder.GeoHashGrid(bucketAgg.ID, bucketAgg.Field, func(a *es.GeoHashGridAggregation, b es.AggBuilder) { - a.Precision = stringToIntWithDefaultValue(bucketAgg.Settings.Get("precision").MustString(), es.DefaultGeoHashPrecision) - aggBuilder = b - }) - - return aggBuilder -} - -func getPipelineAggField(m *MetricAgg) string { - // In frontend we are using Field as pipelineAggField - // There might be historical reason why in backend we were using PipelineAggregate as pipelineAggField - // So for now let's check Field first and then PipelineAggregate to ensure that we are not breaking anything - // TODO: Investigate, if we can remove check for PipelineAggregate - pipelineAggField := m.Field - - if pipelineAggField == "" { - pipelineAggField = m.PipelineAggregate - } - return pipelineAggField -} - -func isQueryWithError(query *Query) error { - if len(query.BucketAggs) == 0 { - // If no aggregations, only document and logs queries are valid - if len(query.Metrics) == 0 || (!isLogsQuery(query) && !isDocumentQuery(query)) { - return fmt.Errorf("invalid query, missing metrics and aggregations") - } - } - return nil -} - -func isLogsQuery(query *Query) bool { - return query.Metrics[0].Type == logsType -} - -func isDocumentQuery(query *Query) bool { - return isRawDataQuery(query) || isRawDocumentQuery(query) -} - -func isRawDataQuery(query *Query) bool { - return query.Metrics[0].Type == rawDataType -} - -func isRawDocumentQuery(query *Query) bool { - return query.Metrics[0].Type == rawDocumentType -} - -func processLogsQuery(q *Query, b *es.SearchRequestBuilder, from, to int64, defaultTimeField string) { - metric := q.Metrics[0] - sort := es.SortOrderDesc - if metric.Settings.Get("sortDirection").MustString() == "asc" { - // This is currently used only for log context query - sort = es.SortOrderAsc - } - b.Sort(sort, defaultTimeField, "boolean") - b.Sort(sort, "_doc", "") - b.AddDocValueField(defaultTimeField) - // We need to add timeField as field with standardized time format to not receive - // invalid formats that elasticsearch can parse, but our frontend can't (e.g. yyyy_MM_dd_HH_mm_ss) - b.AddTimeFieldWithStandardizedFormat(defaultTimeField) - b.Size(stringToIntWithDefaultValue(metric.Settings.Get("limit").MustString(), defaultSize)) - b.AddHighlight() - - // This is currently used only for log context query to get - // log lines before and after the selected log line - searchAfter := metric.Settings.Get("searchAfter").MustArray() - for _, value := range searchAfter { - b.AddSearchAfter(value) - } - - // For log query, we add a date histogram aggregation - aggBuilder := b.Agg() - q.BucketAggs = append(q.BucketAggs, &BucketAgg{ - Type: dateHistType, - Field: defaultTimeField, - ID: "1", - Settings: simplejson.NewFromAny(map[string]any{ - "interval": "auto", - }), - }) - bucketAgg := q.BucketAggs[0] - bucketAgg.Settings = simplejson.NewFromAny( - bucketAgg.generateSettingsForDSL(), - ) - _ = addDateHistogramAgg(aggBuilder, bucketAgg, from, to, defaultTimeField) -} - -func processDocumentQuery(q *Query, b *es.SearchRequestBuilder, from, to int64, defaultTimeField string) { - metric := q.Metrics[0] - b.Sort(es.SortOrderDesc, defaultTimeField, "boolean") - b.Sort(es.SortOrderDesc, "_doc", "") - b.AddDocValueField(defaultTimeField) - if isRawDataQuery(q) { - // For raw_data queries we need to add timeField as field with standardized time format to not receive - // invalid formats that elasticsearch can parse, but our frontend can't (e.g. yyyy_MM_dd_HH_mm_ss) - b.AddTimeFieldWithStandardizedFormat(defaultTimeField) - } - b.Size(stringToIntWithDefaultValue(metric.Settings.Get("size").MustString(), defaultSize)) -} - -func processTimeSeriesQuery(q *Query, b *es.SearchRequestBuilder, from, to int64, defaultTimeField string) { - aggBuilder := b.Agg() - // Process buckets - // iterate backwards to create aggregations bottom-down - for _, bucketAgg := range q.BucketAggs { - bucketAgg.Settings = simplejson.NewFromAny( - bucketAgg.generateSettingsForDSL(), - ) - switch bucketAgg.Type { - case dateHistType: - aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to, defaultTimeField) - case histogramType: - aggBuilder = addHistogramAgg(aggBuilder, bucketAgg) - case filtersType: - aggBuilder = addFiltersAgg(aggBuilder, bucketAgg) - case termsType: - aggBuilder = addTermsAgg(aggBuilder, bucketAgg, q.Metrics) - case geohashGridType: - aggBuilder = addGeoHashGridAgg(aggBuilder, bucketAgg) - case nestedType: - aggBuilder = addNestedAgg(aggBuilder, bucketAgg) - } - } - - // Process metrics - for _, m := range q.Metrics { - m := m - - if m.Type == countType { - continue - } - - if isPipelineAgg(m.Type) { - if isPipelineAggWithMultipleBucketPaths(m.Type) { - if len(m.PipelineVariables) > 0 { - bucketPaths := map[string]any{} - for name, pipelineAgg := range m.PipelineVariables { - if _, err := strconv.Atoi(pipelineAgg); err == nil { - var appliedAgg *MetricAgg - for _, pipelineMetric := range q.Metrics { - if pipelineMetric.ID == pipelineAgg { - appliedAgg = pipelineMetric - break - } - } - if appliedAgg != nil { - if appliedAgg.Type == countType { - bucketPaths[name] = "_count" - } else { - bucketPaths[name] = pipelineAgg - } - } - } - } - - aggBuilder.Pipeline(m.ID, m.Type, bucketPaths, func(a *es.PipelineAggregation) { - a.Settings = m.generateSettingsForDSL() - }) - } else { - continue - } - } else { - pipelineAggField := getPipelineAggField(m) - if _, err := strconv.Atoi(pipelineAggField); err == nil { - var appliedAgg *MetricAgg - for _, pipelineMetric := range q.Metrics { - if pipelineMetric.ID == pipelineAggField { - appliedAgg = pipelineMetric - break - } - } - if appliedAgg != nil { - bucketPath := pipelineAggField - if appliedAgg.Type == countType { - bucketPath = "_count" - } - - aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) { - a.Settings = m.generateSettingsForDSL() - }) - } - } else { - continue - } - } - } else { - aggBuilder.Metric(m.ID, m.Type, m.Field, func(a *es.MetricAggregation) { - a.Settings = m.generateSettingsForDSL() - }) - } - } -} - -func stringToIntWithDefaultValue(valueStr string, defaultValue int) int { - value, err := strconv.Atoi(valueStr) - if err != nil { - value = defaultValue - } - // In our case, 0 is not a valid value and in this case we default to defaultValue - if value == 0 { - value = defaultValue - } - return value -} - -func stringToFloatWithDefaultValue(valueStr string, defaultValue float64) float64 { - value, err := strconv.ParseFloat(valueStr, 64) - if err != nil { - value = defaultValue - } - // In our case, 0 is not a valid value and in this case we default to defaultValue - if value == 0 { - value = defaultValue - } - return value -} diff --git a/pkg/tsdb/elasticsearch/data_query_processor.go b/pkg/tsdb/elasticsearch/data_query_processor.go new file mode 100644 index 00000000000..5f6ea448ddd --- /dev/null +++ b/pkg/tsdb/elasticsearch/data_query_processor.go @@ -0,0 +1,199 @@ +package elasticsearch + +import ( + "fmt" + "strconv" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" +) + +// processQuery processes a single query and adds it to the multi-search request builder +func (e *elasticsearchDataQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilder, from, to int64) error { + err := isQueryWithError(q) + if err != nil { + return backend.DownstreamError(fmt.Errorf("received invalid query. %w", err)) + } + + defaultTimeField := e.client.GetConfiguredFields().TimeField + b := ms.Search(q.Interval, q.TimeRange) + b.Size(0) + filters := b.Query().Bool().Filter() + filters.AddDateRangeFilter(defaultTimeField, to, from, es.DateFormatEpochMS) + filters.AddQueryStringFilter(q.RawQuery, true) + + if isLogsQuery(q) { + processLogsQuery(q, b, from, to, defaultTimeField) + } else if isDocumentQuery(q) { + processDocumentQuery(q, b, from, to, defaultTimeField) + } else { + // Otherwise, it is a time series query and we process it + processTimeSeriesQuery(q, b, from, to, defaultTimeField) + } + + return nil +} + +// processLogsQuery processes a logs query and configures the search request accordingly +func processLogsQuery(q *Query, b *es.SearchRequestBuilder, from, to int64, defaultTimeField string) { + metric := q.Metrics[0] + sort := es.SortOrderDesc + if metric.Settings.Get("sortDirection").MustString() == "asc" { + // This is currently used only for log context query + sort = es.SortOrderAsc + } + b.Sort(sort, defaultTimeField, "boolean") + b.Sort(sort, "_doc", "") + b.AddDocValueField(defaultTimeField) + // We need to add timeField as field with standardized time format to not receive + // invalid formats that elasticsearch can parse, but our frontend can't (e.g. yyyy_MM_dd_HH_mm_ss) + b.AddTimeFieldWithStandardizedFormat(defaultTimeField) + b.Size(stringToIntWithDefaultValue(metric.Settings.Get("limit").MustString(), defaultSize)) + b.AddHighlight() + + // This is currently used only for log context query to get + // log lines before and after the selected log line + searchAfter := metric.Settings.Get("searchAfter").MustArray() + for _, value := range searchAfter { + b.AddSearchAfter(value) + } + + // For log query, we add a date histogram aggregation + aggBuilder := b.Agg() + q.BucketAggs = append(q.BucketAggs, &BucketAgg{ + Type: dateHistType, + Field: defaultTimeField, + ID: "1", + Settings: simplejson.NewFromAny(map[string]any{ + "interval": "auto", + }), + }) + bucketAgg := q.BucketAggs[0] + bucketAgg.Settings = simplejson.NewFromAny( + bucketAgg.generateSettingsForDSL(), + ) + _ = addDateHistogramAgg(aggBuilder, bucketAgg, from, to, defaultTimeField) +} + +// processDocumentQuery processes a document query (raw_data or raw_document) and configures the search request +func processDocumentQuery(q *Query, b *es.SearchRequestBuilder, from, to int64, defaultTimeField string) { + metric := q.Metrics[0] + b.Sort(es.SortOrderDesc, defaultTimeField, "boolean") + b.Sort(es.SortOrderDesc, "_doc", "") + b.AddDocValueField(defaultTimeField) + if isRawDataQuery(q) { + // For raw_data queries we need to add timeField as field with standardized time format to not receive + // invalid formats that elasticsearch can parse, but our frontend can't (e.g. yyyy_MM_dd_HH_mm_ss) + b.AddTimeFieldWithStandardizedFormat(defaultTimeField) + } + b.Size(stringToIntWithDefaultValue(metric.Settings.Get("size").MustString(), defaultSize)) +} + +// processTimeSeriesQuery processes a time series query with aggregations and metrics +func processTimeSeriesQuery(q *Query, b *es.SearchRequestBuilder, from, to int64, defaultTimeField string) { + aggBuilder := b.Agg() + // Process buckets + // iterate backwards to create aggregations bottom-down + for _, bucketAgg := range q.BucketAggs { + bucketAgg.Settings = simplejson.NewFromAny( + bucketAgg.generateSettingsForDSL(), + ) + switch bucketAgg.Type { + case dateHistType: + aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to, defaultTimeField) + case histogramType: + aggBuilder = addHistogramAgg(aggBuilder, bucketAgg) + case filtersType: + aggBuilder = addFiltersAgg(aggBuilder, bucketAgg) + case termsType: + aggBuilder = addTermsAgg(aggBuilder, bucketAgg, q.Metrics) + case geohashGridType: + aggBuilder = addGeoHashGridAgg(aggBuilder, bucketAgg) + case nestedType: + aggBuilder = addNestedAgg(aggBuilder, bucketAgg) + } + } + + // Process metrics + for _, m := range q.Metrics { + m := m + + if m.Type == countType { + continue + } + + if isPipelineAgg(m.Type) { + if isPipelineAggWithMultipleBucketPaths(m.Type) { + if len(m.PipelineVariables) > 0 { + bucketPaths := map[string]any{} + for name, pipelineAgg := range m.PipelineVariables { + if _, err := strconv.Atoi(pipelineAgg); err == nil { + var appliedAgg *MetricAgg + for _, pipelineMetric := range q.Metrics { + if pipelineMetric.ID == pipelineAgg { + appliedAgg = pipelineMetric + break + } + } + if appliedAgg != nil { + if appliedAgg.Type == countType { + bucketPaths[name] = "_count" + } else { + bucketPaths[name] = pipelineAgg + } + } + } + } + + aggBuilder.Pipeline(m.ID, m.Type, bucketPaths, func(a *es.PipelineAggregation) { + a.Settings = m.generateSettingsForDSL() + }) + } else { + continue + } + } else { + pipelineAggField := getPipelineAggField(m) + if _, err := strconv.Atoi(pipelineAggField); err == nil { + var appliedAgg *MetricAgg + for _, pipelineMetric := range q.Metrics { + if pipelineMetric.ID == pipelineAggField { + appliedAgg = pipelineMetric + break + } + } + if appliedAgg != nil { + bucketPath := pipelineAggField + if appliedAgg.Type == countType { + bucketPath = "_count" + } + + aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) { + a.Settings = m.generateSettingsForDSL() + }) + } + } else { + continue + } + } + } else { + aggBuilder.Metric(m.ID, m.Type, m.Field, func(a *es.MetricAggregation) { + a.Settings = m.generateSettingsForDSL() + }) + } + } +} + +// getPipelineAggField returns the pipeline aggregation field +func getPipelineAggField(m *MetricAgg) string { + // In frontend we are using Field as pipelineAggField + // There might be historical reason why in backend we were using PipelineAggregate as pipelineAggField + // So for now let's check Field first and then PipelineAggregate to ensure that we are not breaking anything + // TODO: Investigate, if we can remove check for PipelineAggregate + pipelineAggField := m.Field + + if pipelineAggField == "" { + pipelineAggField = m.PipelineAggregate + } + return pipelineAggField +} diff --git a/pkg/tsdb/elasticsearch/data_query_settings.go b/pkg/tsdb/elasticsearch/data_query_settings.go new file mode 100644 index 00000000000..fe286ccaeda --- /dev/null +++ b/pkg/tsdb/elasticsearch/data_query_settings.go @@ -0,0 +1,87 @@ +package elasticsearch + +import ( + "strconv" + + "github.com/grafana/grafana/pkg/components/simplejson" +) + +// setFloatPath converts a string value at the specified path to float64 +func setFloatPath(settings *simplejson.Json, path ...string) { + if stringValue, err := settings.GetPath(path...).String(); err == nil { + if value, err := strconv.ParseFloat(stringValue, 64); err == nil { + settings.SetPath(path, value) + } + } +} + +// setIntPath converts a string value at the specified path to int64 +func setIntPath(settings *simplejson.Json, path ...string) { + if stringValue, err := settings.GetPath(path...).String(); err == nil { + if value, err := strconv.ParseInt(stringValue, 10, 64); err == nil { + settings.SetPath(path, value) + } + } +} + +// generateSettingsForDSL casts values to float when required by Elastic's query DSL for MetricAgg +func (metricAggregation MetricAgg) generateSettingsForDSL() map[string]any { + switch metricAggregation.Type { + case "moving_avg": + setFloatPath(metricAggregation.Settings, "window") + setFloatPath(metricAggregation.Settings, "predict") + setFloatPath(metricAggregation.Settings, "settings", "alpha") + setFloatPath(metricAggregation.Settings, "settings", "beta") + setFloatPath(metricAggregation.Settings, "settings", "gamma") + setFloatPath(metricAggregation.Settings, "settings", "period") + case "serial_diff": + setFloatPath(metricAggregation.Settings, "lag") + } + + if isMetricAggregationWithInlineScriptSupport(metricAggregation.Type) { + scriptValue, err := metricAggregation.Settings.GetPath("script").String() + if err != nil { + // the script is stored using the old format : `script:{inline: "value"}` or is not set + scriptValue, err = metricAggregation.Settings.GetPath("script", "inline").String() + } + + if err == nil { + metricAggregation.Settings.SetPath([]string{"script"}, scriptValue) + } + } + + return metricAggregation.Settings.MustMap() +} + +// generateSettingsForDSL converts bucket aggregation settings to DSL format +func (bucketAgg BucketAgg) generateSettingsForDSL() map[string]any { + setIntPath(bucketAgg.Settings, "min_doc_count") + + return bucketAgg.Settings.MustMap() +} + +// stringToIntWithDefaultValue converts a string to int with a default fallback value +func stringToIntWithDefaultValue(valueStr string, defaultValue int) int { + value, err := strconv.Atoi(valueStr) + if err != nil { + value = defaultValue + } + // In our case, 0 is not a valid value and in this case we default to defaultValue + if value == 0 { + value = defaultValue + } + return value +} + +// stringToFloatWithDefaultValue converts a string to float64 with a default fallback value +func stringToFloatWithDefaultValue(valueStr string, defaultValue float64) float64 { + value, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + value = defaultValue + } + // In our case, 0 is not a valid value and in this case we default to defaultValue + if value == 0 { + value = defaultValue + } + return value +} diff --git a/pkg/tsdb/elasticsearch/data_query_validator.go b/pkg/tsdb/elasticsearch/data_query_validator.go new file mode 100644 index 00000000000..def537c02da --- /dev/null +++ b/pkg/tsdb/elasticsearch/data_query_validator.go @@ -0,0 +1,36 @@ +package elasticsearch + +import ( + "fmt" +) + +// isQueryWithError validates the query and returns an error if invalid +func isQueryWithError(query *Query) error { + if len(query.BucketAggs) == 0 { + // If no aggregations, only document and logs queries are valid + if len(query.Metrics) == 0 || (!isLogsQuery(query) && !isDocumentQuery(query)) { + return fmt.Errorf("invalid query, missing metrics and aggregations") + } + } + return nil +} + +// isLogsQuery checks if the query is a logs query +func isLogsQuery(query *Query) bool { + return len(query.Metrics) > 0 && query.Metrics[0].Type == logsType +} + +// isDocumentQuery checks if the query is a document query (raw_data or raw_document) +func isDocumentQuery(query *Query) bool { + return isRawDataQuery(query) || isRawDocumentQuery(query) +} + +// isRawDataQuery checks if the query is a raw_data query +func isRawDataQuery(query *Query) bool { + return len(query.Metrics) > 0 && query.Metrics[0].Type == rawDataType +} + +// isRawDocumentQuery checks if the query is a raw_document query +func isRawDocumentQuery(query *Query) bool { + return len(query.Metrics) > 0 && query.Metrics[0].Type == rawDocumentType +} diff --git a/pkg/tsdb/elasticsearch/dataframe_builder.go b/pkg/tsdb/elasticsearch/dataframe_builder.go new file mode 100644 index 00000000000..891cc9f55d6 --- /dev/null +++ b/pkg/tsdb/elasticsearch/dataframe_builder.go @@ -0,0 +1,155 @@ +package elasticsearch + +import ( + "encoding/json" + "sort" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" +) + +// processDocsToDataFrameFields converts documents to data frame fields +func processDocsToDataFrameFields(docs []map[string]interface{}, propNames []string, configuredFields es.ConfiguredFields) []*data.Field { + size := len(docs) + isFilterable := true + allFields := make([]*data.Field, len(propNames)) + timeString := "" + timeStringOk := false + + for propNameIdx, propName := range propNames { + // Special handling for time field + if propName == configuredFields.TimeField { + timeVector := make([]*time.Time, size) + for i, doc := range docs { + // Check if time field is a string + timeString, timeStringOk = doc[configuredFields.TimeField].(string) + // If not, it might be an array with one time string + if !timeStringOk { + timeList, ok := doc[configuredFields.TimeField].([]interface{}) + if !ok || len(timeList) != 1 { + continue + } + // Check if the first element is a string + timeString, timeStringOk = timeList[0].(string) + if !timeStringOk { + continue + } + } + timeValue, err := time.Parse(time.RFC3339Nano, timeString) + if err != nil { + // We skip time values that cannot be parsed + continue + } else { + timeVector[i] = &timeValue + } + } + field := data.NewField(configuredFields.TimeField, nil, timeVector) + field.Config = &data.FieldConfig{Filterable: &isFilterable} + allFields[propNameIdx] = field + continue + } + + propNameValue := findTheFirstNonNilDocValueForPropName(docs, propName) + switch propNameValue.(type) { + // We are checking for default data types values (float64, int, bool, string) + // and default to json.RawMessage if we cannot find any of them + case float64: + allFields[propNameIdx] = createFieldOfType[float64](docs, propName, size, isFilterable) + case int: + allFields[propNameIdx] = createFieldOfType[int](docs, propName, size, isFilterable) + case string: + allFields[propNameIdx] = createFieldOfType[string](docs, propName, size, isFilterable) + case bool: + allFields[propNameIdx] = createFieldOfType[bool](docs, propName, size, isFilterable) + default: + fieldVector := make([]*json.RawMessage, size) + for i, doc := range docs { + bytes, err := json.Marshal(doc[propName]) + if err != nil { + // We skip values that cannot be marshalled + continue + } + value := json.RawMessage(bytes) + fieldVector[i] = &value + } + field := data.NewField(propName, nil, fieldVector) + field.Config = &data.FieldConfig{Filterable: &isFilterable} + allFields[propNameIdx] = field + } + } + + return allFields +} + +// findTheFirstNonNilDocValueForPropName finds the first non-nil value for propName in docs. +// If none of the values are non-nil, it returns the value of propName in the first doc. +func findTheFirstNonNilDocValueForPropName(docs []map[string]interface{}, propName string) interface{} { + for _, doc := range docs { + if doc[propName] != nil { + return doc[propName] + } + } + return docs[0][propName] +} + +// createFieldOfType creates a data field of the specified type +func createFieldOfType[T int | float64 | bool | string](docs []map[string]interface{}, propName string, size int, isFilterable bool) *data.Field { + fieldVector := make([]*T, size) + for i, doc := range docs { + value, ok := doc[propName].(T) + if !ok { + continue + } + fieldVector[i] = &value + } + field := data.NewField(propName, nil, fieldVector) + field.Config = &data.FieldConfig{Filterable: &isFilterable} + return field +} + +// createFields creates data fields from existing frames or from propKeys +func createFields(frames data.Frames, propKeys []string) []*data.Field { + var fields []*data.Field + have := map[string]bool{} + + // collect existing fields + for _, frame := range frames { + for _, f := range frame.Fields { + fields = append(fields, f) + have[f.Name] = true + } + } + + // add missing prop fields + for _, pk := range propKeys { + if !have[pk] { + fields = append(fields, data.NewField(pk, nil, []*string{})) + } + } + + return fields +} + +// createPropKeys creates a sorted list of property keys from a map +func createPropKeys(props map[string]string) []string { + propKeys := make([]string, 0) + for k := range props { + propKeys = append(propKeys, k) + } + sort.Strings(propKeys) + return propKeys +} + +// getSortedKeys returns sorted keys from a map +func getSortedKeys(data map[string]interface{}) []string { + keys := make([]string, 0, len(data)) + + for k := range data { + keys = append(keys, k) + } + + sort.Strings(keys) + return keys +} diff --git a/pkg/tsdb/elasticsearch/field_namer.go b/pkg/tsdb/elasticsearch/field_namer.go new file mode 100644 index 00000000000..aea016207ce --- /dev/null +++ b/pkg/tsdb/elasticsearch/field_namer.go @@ -0,0 +1,170 @@ +package elasticsearch + +import ( + "sort" + "strings" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" +) + +// nameFields applies naming logic to data frame fields based on query configuration +func nameFields(queryResult backend.DataResponse, target *Query, keepLabelsInResponse bool) { + set := make(map[string]struct{}) + frames := queryResult.Frames + for _, v := range frames { + for _, vv := range v.Fields { + if metricType, exists := vv.Labels["metric"]; exists { + if _, ok := set[metricType]; !ok { + set[metricType] = struct{}{} + } + } + } + } + metricTypeCount := len(set) + for _, frame := range frames { + if frame.Meta != nil && frame.Meta.Type == data.FrameTypeTimeSeriesMulti { + // if it is a time-series-multi, it means it has two columns, one is "time", + // another is "number" + valueField := frame.Fields[1] + fieldName := getFieldName(*valueField, target, metricTypeCount) + // If we need to keep the labels in the response, to prevent duplication in names and to keep + // backward compatibility with alerting and expressions we use DisplayNameFromDS + if keepLabelsInResponse { + if valueField.Config == nil { + valueField.Config = &data.FieldConfig{} + } + valueField.Config.DisplayNameFromDS = fieldName + // If we don't need to keep labels (how frontend mode worked), we use frame.Name and remove labels + } else { + valueField.Labels = nil + frame.Name = fieldName + } + } + } +} + +// getFieldName generates a field name based on the data field and target configuration +func getFieldName(dataField data.Field, target *Query, metricTypeCount int) string { + metricType := dataField.Labels["metric"] + metricName := getMetricName(metricType) + delete(dataField.Labels, "metric") + + field := "" + if v, ok := dataField.Labels["field"]; ok { + field = v + delete(dataField.Labels, "field") + } + + if target.Alias != "" { + frameName := target.Alias + + subMatches := aliasPatternRegex.FindAllStringSubmatch(target.Alias, -1) + for _, subMatch := range subMatches { + group := subMatch[0] + + if len(subMatch) > 1 { + group = subMatch[1] + } + + if strings.Index(group, "term ") == 0 { + frameName = strings.Replace(frameName, subMatch[0], dataField.Labels[group[5:]], 1) + } + if v, ok := dataField.Labels[group]; ok { + frameName = strings.Replace(frameName, subMatch[0], v, 1) + } + if group == "metric" { + frameName = strings.Replace(frameName, subMatch[0], metricName, 1) + } + if group == "field" { + frameName = strings.Replace(frameName, subMatch[0], field, 1) + } + } + + return frameName + } + // todo, if field and pipelineAgg + if isPipelineAgg(metricType) { + if metricType != "" && isPipelineAggWithMultipleBucketPaths(metricType) { + metricID := "" + if v, ok := dataField.Labels["metricId"]; ok { + metricID = v + } + + for _, metric := range target.Metrics { + if metric.ID == metricID { + metricName = metric.Settings.Get("script").MustString() + for name, pipelineAgg := range metric.PipelineVariables { + for _, m := range target.Metrics { + if m.ID == pipelineAgg { + metricName = strings.ReplaceAll(metricName, "params."+name, describeMetric(m.Type, m.Field)) + } + } + } + } + } + } else { + if field != "" { + found := false + for _, metric := range target.Metrics { + if metric.ID == field { + metricName += " " + describeMetric(metric.Type, metric.Field) + found = true + } + } + if !found { + metricName = "Unset" + } + } + } + } else if field != "" { + metricName += " " + field + } + + delete(dataField.Labels, "metricId") + + if len(dataField.Labels) == 0 { + return metricName + } + + name := "" + for _, v := range getSortedLabelValues(dataField.Labels) { + name += v + " " + } + + if metricTypeCount == 1 { + return strings.TrimSpace(name) + } + + return strings.TrimSpace(name) + " " + metricName +} + +// getSortedLabelValues sorts label keys and returns the label values in sorted order +func getSortedLabelValues(labels data.Labels) []string { + keys := make([]string, 0, len(labels)) + for key := range labels { + keys = append(keys, key) + } + + sort.Strings(keys) + + values := make([]string, len(keys)) + for i, key := range keys { + values[i] = labels[key] + } + + return values +} + +// getMetricName returns the display name for a metric type +func getMetricName(metric string) string { + if text, ok := metricAggType[metric]; ok { + return text + } + + if text, ok := extendedStats[metric]; ok { + return text + } + + return metric +} diff --git a/pkg/tsdb/elasticsearch/logs_response_processor.go b/pkg/tsdb/elasticsearch/logs_response_processor.go new file mode 100644 index 00000000000..b381ac430e1 --- /dev/null +++ b/pkg/tsdb/elasticsearch/logs_response_processor.go @@ -0,0 +1,143 @@ +package elasticsearch + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/data" + + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" +) + +// logsResponseProcessor handles processing of logs query responses +type logsResponseProcessor struct { + logger log.Logger +} + +// newLogsResponseProcessor creates a new logs response processor +func newLogsResponseProcessor(logger log.Logger) *logsResponseProcessor { + return &logsResponseProcessor{ + logger: logger, + } +} + +// processLogsResponse processes logs query responses +func (p *logsResponseProcessor) processLogsResponse(res *es.SearchResponse, target *Query, configuredFields es.ConfiguredFields, queryRes *backend.DataResponse) error { + propNames := make(map[string]bool) + docs := make([]map[string]interface{}, len(res.Hits.Hits)) + searchWords := make(map[string]bool) + + for hitIdx, hit := range res.Hits.Hits { + var flattened map[string]interface{} + var sourceString string + if hit["_source"] != nil { + flattened = flatten(hit["_source"].(map[string]interface{}), 10) + sourceMarshalled, err := json.Marshal(flattened) + if err != nil { + return err + } + sourceString = string(sourceMarshalled) + } + + doc := map[string]interface{}{ + "_id": hit["_id"], + "_type": hit["_type"], + "_index": hit["_index"], + "sort": hit["sort"], + "highlight": hit["highlight"], + // In case of logs query we want to have the raw source as a string field so it can be visualized in logs panel + "_source": sourceString, + } + + for k, v := range flattened { + if configuredFields.LogLevelField != "" && k == configuredFields.LogLevelField { + doc["level"] = v + } else { + doc[k] = v + } + } + + if hit["fields"] != nil { + source, ok := hit["fields"].(map[string]interface{}) + if ok { + for k, v := range source { + doc[k] = v + } + } + } + + // we are going to add an `id` field with the concatenation of `_id` and `_index` + _, ok := doc["id"] + if !ok { + doc["id"] = fmt.Sprintf("%v#%v", doc["_index"], doc["_id"]) + } + + for key := range doc { + propNames[key] = true + } + + // Process highlight to searchWords + if highlights, ok := doc["highlight"].(map[string]interface{}); ok { + for _, highlight := range highlights { + if highlightList, ok := highlight.([]interface{}); ok { + for _, highlightValue := range highlightList { + str := fmt.Sprintf("%v", highlightValue) + matches := searchWordsRegex.FindAllStringSubmatch(str, -1) + + for _, v := range matches { + searchWords[v[1]] = true + } + } + } + } + } + + docs[hitIdx] = doc + } + + sortedPropNames := sortPropNames(propNames, configuredFields, true) + fields := processDocsToDataFrameFields(docs, sortedPropNames, configuredFields) + + frames := data.Frames{} + frame := data.NewFrame("", fields...) + setPreferredVisType(frame, data.VisTypeLogs) + + var total int + if res.Hits.Total != nil { + total = res.Hits.Total.Value + } + setLogsCustomMeta(frame, searchWords, stringToIntWithDefaultValue(target.Metrics[0].Settings.Get("limit").MustString(), defaultSize), total) + frames = append(frames, frame) + queryRes.Frames = frames + + p.logger.Debug("Processed log query response", "fieldsLength", len(frame.Fields)) + return nil +} + +// setLogsCustomMeta sets custom metadata for logs frames +func setLogsCustomMeta(frame *data.Frame, searchWords map[string]bool, limit int, total int) { + i := 0 + searchWordsList := make([]string, len(searchWords)) + for searchWord := range searchWords { + searchWordsList[i] = searchWord + i++ + } + sort.Strings(searchWordsList) + + if frame.Meta == nil { + frame.Meta = &data.FrameMeta{} + } + + if frame.Meta.Custom == nil { + frame.Meta.Custom = map[string]interface{}{} + } + + frame.Meta.Custom = map[string]interface{}{ + "searchWords": searchWordsList, + "limit": limit, + "total": total, + } +} diff --git a/pkg/tsdb/elasticsearch/metrics_response_processor.go b/pkg/tsdb/elasticsearch/metrics_response_processor.go new file mode 100644 index 00000000000..1e60a732d64 --- /dev/null +++ b/pkg/tsdb/elasticsearch/metrics_response_processor.go @@ -0,0 +1,725 @@ +package elasticsearch + +import ( + "fmt" + "sort" + "strconv" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/components/simplejson" +) + +// metricsResponseProcessor handles processing of metrics query responses +type metricsResponseProcessor struct{} + +// newMetricsResponseProcessor creates a new metrics response processor +func newMetricsResponseProcessor() *metricsResponseProcessor { + return &metricsResponseProcessor{} +} + +// processBuckets processes aggregation buckets recursively +func (p *metricsResponseProcessor) processBuckets(aggs map[string]interface{}, target *Query, + queryResult *backend.DataResponse, props map[string]string, depth int) error { + var err error + maxDepth := len(target.BucketAggs) - 1 + + aggIDs := make([]string, 0) + for k := range aggs { + aggIDs = append(aggIDs, k) + } + sort.Strings(aggIDs) + for _, aggID := range aggIDs { + v := aggs[aggID] + aggDef, _ := findAgg(target, aggID) + esAgg := simplejson.NewFromAny(v) + if aggDef == nil { + continue + } + if aggDef.Type == nestedType { + err = p.processBuckets(esAgg.MustMap(), target, queryResult, props, depth+1) + if err != nil { + return err + } + continue + } + + if depth == maxDepth { + if aggDef.Type == dateHistType { + err = p.processMetrics(esAgg, target, queryResult, props) + } else { + err = p.processAggregationDocs(esAgg, aggDef, target, queryResult, props) + } + if err != nil { + return err + } + } else { + for _, b := range esAgg.Get("buckets").MustArray() { + bucket := simplejson.NewFromAny(b) + newProps := make(map[string]string) + + for k, v := range props { + newProps[k] = v + } + + if key, err := bucket.Get("key").String(); err == nil { + newProps[aggDef.Field] = key + } else if key, err := bucket.Get("key").Int64(); err == nil { + newProps[aggDef.Field] = strconv.FormatInt(key, 10) + } + + if key, err := bucket.Get("key_as_string").String(); err == nil { + newProps[aggDef.Field] = key + } + err = p.processBuckets(bucket.MustMap(), target, queryResult, newProps, depth+1) + if err != nil { + return err + } + } + + buckets := esAgg.Get("buckets").MustMap() + bucketKeys := make([]string, 0) + for k := range buckets { + bucketKeys = append(bucketKeys, k) + } + sort.Strings(bucketKeys) + + for _, bucketKey := range bucketKeys { + bucket := simplejson.NewFromAny(buckets[bucketKey]) + newProps := make(map[string]string) + + for k, v := range props { + newProps[k] = v + } + + newProps["filter"] = bucketKey + + err = p.processBuckets(bucket.MustMap(), target, queryResult, newProps, depth+1) + if err != nil { + return err + } + } + } + } + return nil +} + +// processMetrics processes metric aggregations from date histogram buckets +func (p *metricsResponseProcessor) processMetrics(esAgg *simplejson.Json, target *Query, query *backend.DataResponse, + props map[string]string) error { + frames := data.Frames{} + esAggBuckets := esAgg.Get("buckets").MustArray() + + jsonBuckets := make([]*simplejson.Json, len(esAggBuckets)) + + for i, v := range esAggBuckets { + jsonBuckets[i] = simplejson.NewFromAny(v) + } + + for _, metric := range target.Metrics { + if metric.Hide { + continue + } + + switch metric.Type { + case countType: + countFrames, err := p.processCountMetric(jsonBuckets, props) + if err != nil { + return fmt.Errorf("error processing count metric: %w", err) + } + frames = append(frames, countFrames...) + case percentilesType: + percentileFrames, err := p.processPercentilesMetric(metric, jsonBuckets, props) + if err != nil { + return fmt.Errorf("error processing percentiles metric: %w", err) + } + frames = append(frames, percentileFrames...) + case topMetricsType: + topMetricsFrames, err := p.processTopMetricsMetric(metric, jsonBuckets, props) + if err != nil { + return fmt.Errorf("error processing top metrics metric: %w", err) + } + frames = append(frames, topMetricsFrames...) + case extendedStatsType: + extendedStatsFrames, err := p.processExtendedStatsMetric(metric, jsonBuckets, props) + if err != nil { + return fmt.Errorf("error processing extended stats metric: %w", err) + } + + frames = append(frames, extendedStatsFrames...) + default: + defaultFrames, err := p.processDefaultMetric(metric, jsonBuckets, props) + if err != nil { + return fmt.Errorf("error processing default metric: %w", err) + } + frames = append(frames, defaultFrames...) + } + } + if query.Frames != nil { + oldFrames := query.Frames + frames = append(oldFrames, frames...) + } + query.Frames = frames + return nil +} + +// processCountMetric processes count metric aggregations +func (p *metricsResponseProcessor) processCountMetric(buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { + tags := make(map[string]string, len(props)) + timeVector := make([]time.Time, 0, len(buckets)) + values := make([]*float64, 0, len(buckets)) + + for _, bucket := range buckets { + value := castToFloat(bucket.Get("doc_count")) + timeValue, err := getAsTime(bucket.Get("key")) + if err != nil { + return nil, err + } + timeVector = append(timeVector, timeValue) + values = append(values, value) + } + + for k, v := range props { + tags[k] = v + } + tags["metric"] = countType + return data.Frames{newTimeSeriesFrame(timeVector, tags, values)}, nil +} + +// processPercentilesMetric processes percentiles metric aggregations +func (p *metricsResponseProcessor) processPercentilesMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { + if len(buckets) == 0 { + return data.Frames{}, nil + } + + firstBucket := buckets[0] + percentiles := firstBucket.GetPath(metric.ID, "values").MustMap() + + percentileKeys := make([]string, 0) + for k := range percentiles { + percentileKeys = append(percentileKeys, k) + } + sort.Strings(percentileKeys) + + frames := data.Frames{} + + for _, percentileName := range percentileKeys { + tags := make(map[string]string, len(props)) + timeVector := make([]time.Time, 0, len(buckets)) + values := make([]*float64, 0, len(buckets)) + + for k, v := range props { + tags[k] = v + } + tags["metric"] = "p" + percentileName + tags["field"] = metric.Field + for _, bucket := range buckets { + value := castToFloat(bucket.GetPath(metric.ID, "values", percentileName)) + key := bucket.Get("key") + timeValue, err := getAsTime(key) + if err != nil { + return nil, err + } + timeVector = append(timeVector, timeValue) + values = append(values, value) + } + frames = append(frames, newTimeSeriesFrame(timeVector, tags, values)) + } + + return frames, nil +} + +// processTopMetricsMetric processes top_metrics metric aggregations +func (p *metricsResponseProcessor) processTopMetricsMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { + metrics := metric.Settings.Get("metrics").MustArray() + + frames := data.Frames{} + + for _, metricField := range metrics { + tags := make(map[string]string, len(props)) + timeVector := make([]time.Time, 0, len(buckets)) + values := make([]*float64, 0, len(buckets)) + for k, v := range props { + tags[k] = v + } + + tags["field"] = metricField.(string) + tags["metric"] = "top_metrics" + + for _, bucket := range buckets { + stats := bucket.GetPath(metric.ID, "top") + timeValue, err := getAsTime(bucket.Get("key")) + if err != nil { + return nil, err + } + timeVector = append(timeVector, timeValue) + + for _, stat := range stats.MustArray() { + stat := stat.(map[string]interface{}) + + metrics, hasMetrics := stat["metrics"] + if hasMetrics { + metrics := metrics.(map[string]interface{}) + metricValue, hasMetricValue := metrics[metricField.(string)] + + if hasMetricValue && metricValue != nil { + v := metricValue.(float64) + values = append(values, &v) + } + } + } + } + + frames = append(frames, newTimeSeriesFrame(timeVector, tags, values)) + } + + return frames, nil +} + +// processExtendedStatsMetric processes extended_stats metric aggregations +func (p *metricsResponseProcessor) processExtendedStatsMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { + metaKeys := make([]string, 0) + meta := metric.Meta.MustMap() + for k := range meta { + metaKeys = append(metaKeys, k) + } + sort.Strings(metaKeys) + + frames := data.Frames{} + + for _, statName := range metaKeys { + v := meta[statName] + if enabled, ok := v.(bool); !ok || !enabled { + continue + } + + tags := make(map[string]string, len(props)) + timeVector := make([]time.Time, 0, len(buckets)) + values := make([]*float64, 0, len(buckets)) + + for k, v := range props { + tags[k] = v + } + tags["metric"] = statName + tags["field"] = metric.Field + + for _, bucket := range buckets { + timeValue, err := getAsTime(bucket.Get("key")) + if err != nil { + return nil, err + } + var value *float64 + switch statName { + case "std_deviation_bounds_upper": + value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "upper")) + case "std_deviation_bounds_lower": + value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "lower")) + default: + value = castToFloat(bucket.GetPath(metric.ID, statName)) + } + timeVector = append(timeVector, timeValue) + values = append(values, value) + } + labels := tags + frames = append(frames, newTimeSeriesFrame(timeVector, labels, values)) + } + + return frames, nil +} + +// processDefaultMetric processes default metric aggregations +func (p *metricsResponseProcessor) processDefaultMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { + tags := make(map[string]string, len(props)) + timeVector := make([]time.Time, 0, len(buckets)) + values := make([]*float64, 0, len(buckets)) + + for k, v := range props { + tags[k] = v + } + + tags["metric"] = metric.Type + tags["field"] = metric.Field + tags["metricId"] = metric.ID + for _, bucket := range buckets { + timeValue, err := getAsTime(bucket.Get("key")) + if err != nil { + return nil, err + } + valueObj, err := bucket.Get(metric.ID).Map() + if err != nil { + continue + } + var value *float64 + if _, ok := valueObj["normalized_value"]; ok { + value = castToFloat(bucket.GetPath(metric.ID, "normalized_value")) + } else { + value = castToFloat(bucket.GetPath(metric.ID, "value")) + } + timeVector = append(timeVector, timeValue) + values = append(values, value) + } + return data.Frames{newTimeSeriesFrame(timeVector, tags, values)}, nil +} + +// ensurePropFields guarantees all property columns exist even if prior frames lacked them +func ensurePropFields(fields *[]*data.Field, keys []string) { + have := map[string]bool{} + for _, f := range *fields { + have[f.Name] = true + } + for _, k := range keys { + if !have[k] { + d := "" + f := extractDataField(k, &d) + *fields = append(*fields, f) + } + } +} + +// appendPropsRow appends one row of property values; skipKey avoids double-append +func appendPropsRow(fields *[]*data.Field, props map[string]string, propKeys []string, skipKey string) { + for _, f := range *fields { + for _, pk := range propKeys { + if pk == skipKey { + continue + } + if f.Name == pk { + val := props[pk] + f.Append(&val) + } + } + } +} + +// appendMetrics appends all metric values for a single bucket/row +func appendMetrics(fields *[]*data.Field, bucket *simplejson.Json, target *Query) { + var values []interface{} + for _, metric := range target.Metrics { + switch metric.Type { + case countType: + addMetricValueToFields(fields, values, getMetricName(metric.Type), castToFloat(bucket.Get("doc_count"))) + case extendedStatsType: + addExtendedStatsToFields(fields, bucket, metric, values) + case percentilesType: + addPercentilesToFields(fields, bucket, metric, values) + case topMetricsType: + addTopMetricsToFields(fields, bucket, metric, values) + default: + addOtherMetricsToFields(fields, bucket, metric, values, target) + } + } +} + +// appendKeyColumnString appends a string key to an existing field or creates it +func appendKeyColumnString(fields *[]*data.Field, fieldName, key string) { + for _, f := range *fields { + if f.Name == fieldName { + k := key + f.Append(&k) + return + } + } + k := key + f := extractDataField(fieldName, &k) + f.Append(&k) + *fields = append(*fields, f) +} + +// appendBucketKeyValue appends the bucket's "key" (string or number) to fieldName +func appendBucketKeyValue(fields *[]*data.Field, fieldName string, bucket *simplejson.Json) error { + for _, f := range *fields { + if f.Name == fieldName { + if s, err := bucket.Get("key").String(); err == nil { + f.Append(&s) + return nil + } + num, err := bucket.Get("key").Float64() + if err != nil { + return fmt.Errorf("error appending bucket key to existing field %q: %w", fieldName, err) + } + f.Append(&num) + return nil + } + } + + // field not present yet + if s, err := bucket.Get("key").String(); err == nil { + f := extractDataField(fieldName, &s) + f.Append(&s) + *fields = append(*fields, f) + return nil + } + + num, err := bucket.Get("key").Float64() + if err != nil { + return fmt.Errorf("error appending bucket key to new field %q: %w", fieldName, err) + } + + f := extractDataField(fieldName, &num) + f.Append(&num) + *fields = append(*fields, f) + + return nil +} + +func (p *metricsResponseProcessor) processAggregationDocs( + esAgg *simplejson.Json, + aggDef *BucketAgg, + target *Query, + queryResult *backend.DataResponse, + props map[string]string, +) error { + propKeys := createPropKeys(props) + buckets := esAgg.Get("buckets") + + if arr := buckets.MustArray(); len(arr) > 0 { + fields := createFields(queryResult.Frames, propKeys) + ensurePropFields(&fields, propKeys) + + for _, v := range arr { + bucket := simplejson.NewFromAny(v) + + appendPropsRow(&fields, props, propKeys, "") + if aggDef.Field != "" { + if err := appendBucketKeyValue(&fields, aggDef.Field, bucket); err != nil { + return err + } + } + appendMetrics(&fields, bucket, target) + } + + queryResult.Frames = data.Frames{&data.Frame{Fields: fields}} + return nil + } + + if m := buckets.MustMap(); len(m) > 0 { + // default key column to "filter" for leaf filters + keyFieldName := aggDef.Field + if keyFieldName == "" { + keyFieldName = "filter" + } + + // ensure "filter" exists among props + hasFilter := false + for _, pk := range propKeys { + if pk == "filter" { + hasFilter = true + break + } + } + if !hasFilter { + propKeys = append(propKeys, "filter") + } + + fields := createFields(queryResult.Frames, propKeys) + ensurePropFields(&fields, propKeys) + + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + bucket := simplejson.NewFromAny(m[k]) + + locProps := make(map[string]string, len(props)+1) + for kk, vv := range props { + locProps[kk] = vv + } + locProps["filter"] = k + + // avoid double-append when the key column is "filter" + skip := "" + if keyFieldName == "filter" { + skip = "filter" + } + + appendPropsRow(&fields, locProps, propKeys, skip) + appendKeyColumnString(&fields, keyFieldName, k) + appendMetrics(&fields, bucket, target) + } + + queryResult.Frames = data.Frames{&data.Frame{Fields: fields}} + return nil + } + + // no buckets present + queryResult.Frames = data.Frames{} + return nil +} + +// newTimeSeriesFrame creates a new time series frame +func newTimeSeriesFrame(timeData []time.Time, tags map[string]string, values []*float64) *data.Frame { + frame := data.NewFrame("", + data.NewField(data.TimeSeriesTimeFieldName, nil, timeData), + data.NewField(data.TimeSeriesValueFieldName, tags, values)) + frame.Meta = &data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMulti, + } + return frame +} + +// trimDatapoints trims datapoints from the beginning and end of the results +func trimDatapoints(queryResult backend.DataResponse, target *Query) { + var histogram *BucketAgg + for _, bucketAgg := range target.BucketAggs { + if bucketAgg.Type == dateHistType { + histogram = bucketAgg + break + } + } + + if histogram == nil { + return + } + + trimEdges, err := castToInt(histogram.Settings.Get("trimEdges")) + if err != nil { + return + } + + frames := queryResult.Frames + + for _, frame := range frames { + for _, field := range frame.Fields { + if field.Len() > trimEdges*2 { + // first we delete the first "trim" items + for i := 0; i < trimEdges; i++ { + field.Delete(0) + } + + // then we delete the last "trim" items + for i := 0; i < trimEdges; i++ { + field.Delete(field.Len() - 1) + } + } + } + } +} + +// Helper functions for adding metrics to fields + +func addMetricValueToFields(fields *[]*data.Field, values []interface{}, metricName string, value *float64) { + index := -1 + for i, f := range *fields { + if f.Name == metricName { + index = i + break + } + } + + var field data.Field + if index == -1 { + field = *data.NewField(metricName, nil, []*float64{}) + *fields = append(*fields, &field) + } else { + field = *(*fields)[index] + } + field.Append(value) +} + +func addPercentilesToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}) { + percentiles := bucket.GetPath(metric.ID, "values") + for _, percentileName := range getSortedKeys(percentiles.MustMap()) { + percentileValue := percentiles.Get(percentileName).MustFloat64() + addMetricValueToFields(fields, values, fmt.Sprintf("p%v %v", percentileName, metric.Field), &percentileValue) + } +} + +func addExtendedStatsToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}) { + metaKeys := make([]string, 0) + meta := metric.Meta.MustMap() + for k := range meta { + metaKeys = append(metaKeys, k) + } + sort.Strings(metaKeys) + for _, statName := range metaKeys { + v := meta[statName] + if enabled, ok := v.(bool); !ok || !enabled { + continue + } + var value *float64 + switch statName { + case "std_deviation_bounds_upper": + value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "upper")) + case "std_deviation_bounds_lower": + value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "lower")) + default: + value = castToFloat(bucket.GetPath(metric.ID, statName)) + } + + addMetricValueToFields(fields, values, getMetricName(metric.Type), value) + break + } +} + +func addTopMetricsToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}) { + baseName := getMetricName(metric.Type) + metrics := metric.Settings.Get("metrics").MustStringArray() + for _, metricField := range metrics { + // If we selected more than one metric we also add each metric name + metricName := baseName + if len(metrics) > 1 { + metricName += " " + metricField + } + top := bucket.GetPath(metric.ID, "top").MustArray() + metrics, hasMetrics := top[0].(map[string]interface{})["metrics"] + if hasMetrics { + metrics := metrics.(map[string]interface{}) + metricValue, hasMetricValue := metrics[metricField] + if hasMetricValue && metricValue != nil { + v := metricValue.(float64) + addMetricValueToFields(fields, values, metricName, &v) + } + } + } +} + +func addOtherMetricsToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}, target *Query) { + metricName := getMetricName(metric.Type) + otherMetrics := make([]*MetricAgg, 0) + + for _, m := range target.Metrics { + // To other metrics we add metric of the same type that are not the current metric + if m.ID != metric.ID && m.Type == metric.Type { + otherMetrics = append(otherMetrics, m) + } + } + + if len(otherMetrics) > 0 { + metricName += " " + metric.Field + + // We check if we have metric with the same type and same field name + // If so, append metric.ID to the metric name + for _, m := range otherMetrics { + if m.Field == metric.Field { + metricName += " " + metric.ID + break + } + } + + if metric.Type == "bucket_script" { + // Use the formula in the column name + metricName = metric.Settings.Get("script").MustString("") + } + } + addMetricValueToFields(fields, values, metricName, castToFloat(bucket.GetPath(metric.ID, "value"))) +} + +func extractDataField(name string, v interface{}) *data.Field { + var field *data.Field + switch v.(type) { + case *string: + field = data.NewField(name, nil, []*string{}) + case *float64: + field = data.NewField(name, nil, []*float64{}) + default: + field = &data.Field{} + } + isFilterable := true + field.Config = &data.FieldConfig{Filterable: &isFilterable} + return field +} diff --git a/pkg/tsdb/elasticsearch/raw_response_processor.go b/pkg/tsdb/elasticsearch/raw_response_processor.go new file mode 100644 index 00000000000..f29a608365c --- /dev/null +++ b/pkg/tsdb/elasticsearch/raw_response_processor.go @@ -0,0 +1,133 @@ +package elasticsearch + +import ( + "encoding/json" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/data" + + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" +) + +// rawResponseProcessor handles processing of raw data and raw document query responses +type rawResponseProcessor struct { + logger log.Logger +} + +// newRawResponseProcessor creates a new raw response processor +func newRawResponseProcessor(logger log.Logger) *rawResponseProcessor { + return &rawResponseProcessor{ + logger: logger, + } +} + +// processRawDataResponse processes raw data query responses +func (p *rawResponseProcessor) processRawDataResponse(res *es.SearchResponse, target *Query, configuredFields es.ConfiguredFields, queryRes *backend.DataResponse) error { + propNames := make(map[string]bool) + docs := make([]map[string]interface{}, len(res.Hits.Hits)) + + for hitIdx, hit := range res.Hits.Hits { + var flattened map[string]interface{} + if hit["_source"] != nil { + flattened = flatten(hit["_source"].(map[string]interface{}), 10) + } + + doc := map[string]interface{}{ + "_id": hit["_id"], + "_type": hit["_type"], + "_index": hit["_index"], + "sort": hit["sort"], + "highlight": hit["highlight"], + } + + for k, v := range flattened { + doc[k] = v + } + + if hit["fields"] != nil { + source, ok := hit["fields"].(map[string]interface{}) + if ok { + for k, v := range source { + doc[k] = v + } + } + } + + for key := range doc { + propNames[key] = true + } + + docs[hitIdx] = doc + } + + sortedPropNames := sortPropNames(propNames, configuredFields, false) + fields := processDocsToDataFrameFields(docs, sortedPropNames, configuredFields) + + frames := data.Frames{} + frame := data.NewFrame("", fields...) + + frames = append(frames, frame) + queryRes.Frames = frames + + p.logger.Debug("Processed raw data query response", "fieldsLength", len(frame.Fields)) + return nil +} + +// processRawDocumentResponse processes raw document query responses +func (p *rawResponseProcessor) processRawDocumentResponse(res *es.SearchResponse, target *Query, queryRes *backend.DataResponse) error { + docs := make([]map[string]interface{}, len(res.Hits.Hits)) + for hitIdx, hit := range res.Hits.Hits { + doc := map[string]interface{}{ + "_id": hit["_id"], + "_type": hit["_type"], + "_index": hit["_index"], + "sort": hit["sort"], + "highlight": hit["highlight"], + } + + if hit["_source"] != nil { + source, ok := hit["_source"].(map[string]interface{}) + if ok { + for k, v := range source { + doc[k] = v + } + } + } + + if hit["fields"] != nil { + source, ok := hit["fields"].(map[string]interface{}) + if ok { + for k, v := range source { + doc[k] = v + } + } + } + + docs[hitIdx] = doc + } + + fieldVector := make([]*json.RawMessage, len(res.Hits.Hits)) + for i, doc := range docs { + bytes, err := json.Marshal(doc) + if err != nil { + // We skip docs that can't be marshalled + // should not happen + continue + } + value := json.RawMessage(bytes) + fieldVector[i] = &value + } + + isFilterable := true + field := data.NewField(target.RefID, nil, fieldVector) + field.Config = &data.FieldConfig{Filterable: &isFilterable} + + frames := data.Frames{} + frame := data.NewFrame(target.RefID, field) + frames = append(frames, frame) + + queryRes.Frames = frames + p.logger.Debug("Processed raw document query response", "fieldsLength", len(frame.Fields)) + return nil +} diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 08b951c353a..d05ca92e19b 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -4,11 +4,7 @@ import ( "context" "encoding/json" "errors" - "fmt" "regexp" - "sort" - "strconv" - "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -59,6 +55,11 @@ func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets )) defer span.End() + // Create processors + logsProcessor := newLogsResponseProcessor(logger) + rawProcessor := newRawResponseProcessor(logger) + metricsProcessor := newMetricsResponseProcessor() + for i, res := range responses { _, resSpan := tracing.DefaultTracer().Start(ctx, "datasource.elastic.parseResponse.response", trace.WithAttributes( attribute.String("queryMetricType", targets[i].Metrics[0].Type), @@ -81,21 +82,21 @@ func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets queryRes := backend.DataResponse{} if isRawDataQuery(target) { - err := processRawDataResponse(res, target, configuredFields, &queryRes, logger) + err := rawProcessor.processRawDataResponse(res, target, configuredFields, &queryRes) if err != nil { // TODO: This error never happens so we should remove it return &backend.QueryDataResponse{}, err } result.Responses[target.RefID] = queryRes } else if isRawDocumentQuery(target) { - err := processRawDocumentResponse(res, target, &queryRes, logger) + err := rawProcessor.processRawDocumentResponse(res, target, &queryRes) if err != nil { // TODO: This error never happens so we should remove it return &backend.QueryDataResponse{}, err } result.Responses[target.RefID] = queryRes } else if isLogsQuery(target) { - err := processLogsResponse(res, target, configuredFields, &queryRes, logger) + err := logsProcessor.processLogsResponse(res, target, configuredFields, &queryRes) if err != nil { // TODO: This error never happens so we should remove it return &backend.QueryDataResponse{}, err @@ -104,7 +105,7 @@ func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets } else { // Process as metric query result props := make(map[string]string) - err := processBuckets(res.Aggregations, target, &queryRes, props, 0) + err := metricsProcessor.processBuckets(res.Aggregations, target, &queryRes, props, 0) logger.Debug("Processed metric query response") if err != nil { mt, _ := json.Marshal(target) @@ -129,1081 +130,7 @@ func parseResponse(ctx context.Context, responses []*es.SearchResponse, targets return &result, nil } -func processLogsResponse(res *es.SearchResponse, target *Query, configuredFields es.ConfiguredFields, queryRes *backend.DataResponse, logger log.Logger) error { - propNames := make(map[string]bool) - docs := make([]map[string]interface{}, len(res.Hits.Hits)) - searchWords := make(map[string]bool) - - for hitIdx, hit := range res.Hits.Hits { - var flattened map[string]interface{} - var sourceString string - if hit["_source"] != nil { - flattened = flatten(hit["_source"].(map[string]interface{}), 10) - sourceMarshalled, err := json.Marshal(flattened) - if err != nil { - return err - } - sourceString = string(sourceMarshalled) - } - - doc := map[string]interface{}{ - "_id": hit["_id"], - "_type": hit["_type"], - "_index": hit["_index"], - "sort": hit["sort"], - "highlight": hit["highlight"], - // In case of logs query we want to have the raw source as a string field so it can be visualized in logs panel - "_source": sourceString, - } - - for k, v := range flattened { - if configuredFields.LogLevelField != "" && k == configuredFields.LogLevelField { - doc["level"] = v - } else { - doc[k] = v - } - } - - if hit["fields"] != nil { - source, ok := hit["fields"].(map[string]interface{}) - if ok { - for k, v := range source { - doc[k] = v - } - } - } - - // we are going to add an `id` field with the concatenation of `_id` and `_index` - _, ok := doc["id"] - if !ok { - doc["id"] = fmt.Sprintf("%v#%v", doc["_index"], doc["_id"]) - } - - for key := range doc { - propNames[key] = true - } - - // Process highlight to searchWords - if highlights, ok := doc["highlight"].(map[string]interface{}); ok { - for _, highlight := range highlights { - if highlightList, ok := highlight.([]interface{}); ok { - for _, highlightValue := range highlightList { - str := fmt.Sprintf("%v", highlightValue) - matches := searchWordsRegex.FindAllStringSubmatch(str, -1) - - for _, v := range matches { - searchWords[v[1]] = true - } - } - } - } - } - - docs[hitIdx] = doc - } - - sortedPropNames := sortPropNames(propNames, configuredFields, true) - fields := processDocsToDataFrameFields(docs, sortedPropNames, configuredFields) - - frames := data.Frames{} - frame := data.NewFrame("", fields...) - setPreferredVisType(frame, data.VisTypeLogs) - - var total int - if res.Hits.Total != nil { - total = res.Hits.Total.Value - } - setLogsCustomMeta(frame, searchWords, stringToIntWithDefaultValue(target.Metrics[0].Settings.Get("limit").MustString(), defaultSize), total) - frames = append(frames, frame) - queryRes.Frames = frames - - logger.Debug("Processed log query response", "fieldsLength", len(frame.Fields)) - return nil -} - -func processRawDataResponse(res *es.SearchResponse, target *Query, configuredFields es.ConfiguredFields, queryRes *backend.DataResponse, logger log.Logger) error { - propNames := make(map[string]bool) - docs := make([]map[string]interface{}, len(res.Hits.Hits)) - - for hitIdx, hit := range res.Hits.Hits { - var flattened map[string]interface{} - if hit["_source"] != nil { - flattened = flatten(hit["_source"].(map[string]interface{}), 10) - } - - doc := map[string]interface{}{ - "_id": hit["_id"], - "_type": hit["_type"], - "_index": hit["_index"], - "sort": hit["sort"], - "highlight": hit["highlight"], - } - - for k, v := range flattened { - doc[k] = v - } - - if hit["fields"] != nil { - source, ok := hit["fields"].(map[string]interface{}) - if ok { - for k, v := range source { - doc[k] = v - } - } - } - - for key := range doc { - propNames[key] = true - } - - docs[hitIdx] = doc - } - - sortedPropNames := sortPropNames(propNames, configuredFields, false) - fields := processDocsToDataFrameFields(docs, sortedPropNames, configuredFields) - - frames := data.Frames{} - frame := data.NewFrame("", fields...) - - frames = append(frames, frame) - queryRes.Frames = frames - - logger.Debug("Processed raw data query response", "fieldsLength", len(frame.Fields)) - return nil -} - -func processRawDocumentResponse(res *es.SearchResponse, target *Query, queryRes *backend.DataResponse, logger log.Logger) error { - docs := make([]map[string]interface{}, len(res.Hits.Hits)) - for hitIdx, hit := range res.Hits.Hits { - doc := map[string]interface{}{ - "_id": hit["_id"], - "_type": hit["_type"], - "_index": hit["_index"], - "sort": hit["sort"], - "highlight": hit["highlight"], - } - - if hit["_source"] != nil { - source, ok := hit["_source"].(map[string]interface{}) - if ok { - for k, v := range source { - doc[k] = v - } - } - } - - if hit["fields"] != nil { - source, ok := hit["fields"].(map[string]interface{}) - if ok { - for k, v := range source { - doc[k] = v - } - } - } - - docs[hitIdx] = doc - } - - fieldVector := make([]*json.RawMessage, len(res.Hits.Hits)) - for i, doc := range docs { - bytes, err := json.Marshal(doc) - if err != nil { - // We skip docs that can't be marshalled - // should not happen - continue - } - value := json.RawMessage(bytes) - fieldVector[i] = &value - } - - isFilterable := true - field := data.NewField(target.RefID, nil, fieldVector) - field.Config = &data.FieldConfig{Filterable: &isFilterable} - - frames := data.Frames{} - frame := data.NewFrame(target.RefID, field) - frames = append(frames, frame) - - queryRes.Frames = frames - logger.Debug("Processed raw document query response", "fieldsLength", len(frame.Fields)) - return nil -} - -func processDocsToDataFrameFields(docs []map[string]interface{}, propNames []string, configuredFields es.ConfiguredFields) []*data.Field { - size := len(docs) - isFilterable := true - allFields := make([]*data.Field, len(propNames)) - timeString := "" - timeStringOk := false - - for propNameIdx, propName := range propNames { - // Special handling for time field - if propName == configuredFields.TimeField { - timeVector := make([]*time.Time, size) - for i, doc := range docs { - // Check if time field is a string - timeString, timeStringOk = doc[configuredFields.TimeField].(string) - // If not, it might be an array with one time string - if !timeStringOk { - timeList, ok := doc[configuredFields.TimeField].([]interface{}) - if !ok || len(timeList) != 1 { - continue - } - // Check if the first element is a string - timeString, timeStringOk = timeList[0].(string) - if !timeStringOk { - continue - } - } - timeValue, err := time.Parse(time.RFC3339Nano, timeString) - if err != nil { - // We skip time values that cannot be parsed - continue - } else { - timeVector[i] = &timeValue - } - } - field := data.NewField(configuredFields.TimeField, nil, timeVector) - field.Config = &data.FieldConfig{Filterable: &isFilterable} - allFields[propNameIdx] = field - continue - } - - propNameValue := findTheFirstNonNilDocValueForPropName(docs, propName) - switch propNameValue.(type) { - // We are checking for default data types values (float64, int, bool, string) - // and default to json.RawMessage if we cannot find any of them - case float64: - allFields[propNameIdx] = createFieldOfType[float64](docs, propName, size, isFilterable) - case int: - allFields[propNameIdx] = createFieldOfType[int](docs, propName, size, isFilterable) - case string: - allFields[propNameIdx] = createFieldOfType[string](docs, propName, size, isFilterable) - case bool: - allFields[propNameIdx] = createFieldOfType[bool](docs, propName, size, isFilterable) - default: - fieldVector := make([]*json.RawMessage, size) - for i, doc := range docs { - bytes, err := json.Marshal(doc[propName]) - if err != nil { - // We skip values that cannot be marshalled - continue - } - value := json.RawMessage(bytes) - fieldVector[i] = &value - } - field := data.NewField(propName, nil, fieldVector) - field.Config = &data.FieldConfig{Filterable: &isFilterable} - allFields[propNameIdx] = field - } - } - - return allFields -} - -func processBuckets(aggs map[string]interface{}, target *Query, - queryResult *backend.DataResponse, props map[string]string, depth int) error { - var err error - maxDepth := len(target.BucketAggs) - 1 - - aggIDs := make([]string, 0) - for k := range aggs { - aggIDs = append(aggIDs, k) - } - sort.Strings(aggIDs) - for _, aggID := range aggIDs { - v := aggs[aggID] - aggDef, _ := findAgg(target, aggID) - esAgg := simplejson.NewFromAny(v) - if aggDef == nil { - continue - } - if aggDef.Type == nestedType { - err = processBuckets(esAgg.MustMap(), target, queryResult, props, depth+1) - if err != nil { - return err - } - continue - } - - if depth == maxDepth { - if aggDef.Type == dateHistType { - err = processMetrics(esAgg, target, queryResult, props) - } else { - err = processAggregationDocs(esAgg, aggDef, target, queryResult, props) - } - if err != nil { - return err - } - } else { - for _, b := range esAgg.Get("buckets").MustArray() { - bucket := simplejson.NewFromAny(b) - newProps := make(map[string]string) - - for k, v := range props { - newProps[k] = v - } - - if key, err := bucket.Get("key").String(); err == nil { - newProps[aggDef.Field] = key - } else if key, err := bucket.Get("key").Int64(); err == nil { - newProps[aggDef.Field] = strconv.FormatInt(key, 10) - } - - if key, err := bucket.Get("key_as_string").String(); err == nil { - newProps[aggDef.Field] = key - } - err = processBuckets(bucket.MustMap(), target, queryResult, newProps, depth+1) - if err != nil { - return err - } - } - - buckets := esAgg.Get("buckets").MustMap() - bucketKeys := make([]string, 0) - for k := range buckets { - bucketKeys = append(bucketKeys, k) - } - sort.Strings(bucketKeys) - - for _, bucketKey := range bucketKeys { - bucket := simplejson.NewFromAny(buckets[bucketKey]) - newProps := make(map[string]string) - - for k, v := range props { - newProps[k] = v - } - - newProps["filter"] = bucketKey - - err = processBuckets(bucket.MustMap(), target, queryResult, newProps, depth+1) - if err != nil { - return err - } - } - } - } - return nil -} - -func newTimeSeriesFrame(timeData []time.Time, tags map[string]string, values []*float64) *data.Frame { - frame := data.NewFrame("", - data.NewField(data.TimeSeriesTimeFieldName, nil, timeData), - data.NewField(data.TimeSeriesValueFieldName, tags, values)) - frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMulti, - } - return frame -} - -func processCountMetric(buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { - tags := make(map[string]string, len(props)) - timeVector := make([]time.Time, 0, len(buckets)) - values := make([]*float64, 0, len(buckets)) - - for _, bucket := range buckets { - value := castToFloat(bucket.Get("doc_count")) - timeValue, err := getAsTime(bucket.Get("key")) - if err != nil { - return nil, err - } - timeVector = append(timeVector, timeValue) - values = append(values, value) - } - - for k, v := range props { - tags[k] = v - } - tags["metric"] = countType - return data.Frames{newTimeSeriesFrame(timeVector, tags, values)}, nil -} - -func processPercentilesMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { - if len(buckets) == 0 { - return data.Frames{}, nil - } - - firstBucket := buckets[0] - percentiles := firstBucket.GetPath(metric.ID, "values").MustMap() - - percentileKeys := make([]string, 0) - for k := range percentiles { - percentileKeys = append(percentileKeys, k) - } - sort.Strings(percentileKeys) - - frames := data.Frames{} - - for _, percentileName := range percentileKeys { - tags := make(map[string]string, len(props)) - timeVector := make([]time.Time, 0, len(buckets)) - values := make([]*float64, 0, len(buckets)) - - for k, v := range props { - tags[k] = v - } - tags["metric"] = "p" + percentileName - tags["field"] = metric.Field - for _, bucket := range buckets { - value := castToFloat(bucket.GetPath(metric.ID, "values", percentileName)) - key := bucket.Get("key") - timeValue, err := getAsTime(key) - if err != nil { - return nil, err - } - timeVector = append(timeVector, timeValue) - values = append(values, value) - } - frames = append(frames, newTimeSeriesFrame(timeVector, tags, values)) - } - - return frames, nil -} - -func processTopMetricsMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { - metrics := metric.Settings.Get("metrics").MustArray() - - frames := data.Frames{} - - for _, metricField := range metrics { - tags := make(map[string]string, len(props)) - timeVector := make([]time.Time, 0, len(buckets)) - values := make([]*float64, 0, len(buckets)) - for k, v := range props { - tags[k] = v - } - - tags["field"] = metricField.(string) - tags["metric"] = "top_metrics" - - for _, bucket := range buckets { - stats := bucket.GetPath(metric.ID, "top") - timeValue, err := getAsTime(bucket.Get("key")) - if err != nil { - return nil, err - } - timeVector = append(timeVector, timeValue) - - for _, stat := range stats.MustArray() { - stat := stat.(map[string]interface{}) - - metrics, hasMetrics := stat["metrics"] - if hasMetrics { - metrics := metrics.(map[string]interface{}) - metricValue, hasMetricValue := metrics[metricField.(string)] - - if hasMetricValue && metricValue != nil { - v := metricValue.(float64) - values = append(values, &v) - } - } - } - } - - frames = append(frames, newTimeSeriesFrame(timeVector, tags, values)) - } - - return frames, nil -} - -func processExtendedStatsMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { - metaKeys := make([]string, 0) - meta := metric.Meta.MustMap() - for k := range meta { - metaKeys = append(metaKeys, k) - } - sort.Strings(metaKeys) - - frames := data.Frames{} - - for _, statName := range metaKeys { - v := meta[statName] - if enabled, ok := v.(bool); !ok || !enabled { - continue - } - - tags := make(map[string]string, len(props)) - timeVector := make([]time.Time, 0, len(buckets)) - values := make([]*float64, 0, len(buckets)) - - for k, v := range props { - tags[k] = v - } - tags["metric"] = statName - tags["field"] = metric.Field - - for _, bucket := range buckets { - timeValue, err := getAsTime(bucket.Get("key")) - if err != nil { - return nil, err - } - var value *float64 - switch statName { - case "std_deviation_bounds_upper": - value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "upper")) - case "std_deviation_bounds_lower": - value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "lower")) - default: - value = castToFloat(bucket.GetPath(metric.ID, statName)) - } - timeVector = append(timeVector, timeValue) - values = append(values, value) - } - labels := tags - frames = append(frames, newTimeSeriesFrame(timeVector, labels, values)) - } - - return frames, nil -} - -func processDefaultMetric(metric *MetricAgg, buckets []*simplejson.Json, props map[string]string) (data.Frames, error) { - tags := make(map[string]string, len(props)) - timeVector := make([]time.Time, 0, len(buckets)) - values := make([]*float64, 0, len(buckets)) - - for k, v := range props { - tags[k] = v - } - - tags["metric"] = metric.Type - tags["field"] = metric.Field - tags["metricId"] = metric.ID - for _, bucket := range buckets { - timeValue, err := getAsTime(bucket.Get("key")) - if err != nil { - return nil, err - } - valueObj, err := bucket.Get(metric.ID).Map() - if err != nil { - continue - } - var value *float64 - if _, ok := valueObj["normalized_value"]; ok { - value = castToFloat(bucket.GetPath(metric.ID, "normalized_value")) - } else { - value = castToFloat(bucket.GetPath(metric.ID, "value")) - } - timeVector = append(timeVector, timeValue) - values = append(values, value) - } - return data.Frames{newTimeSeriesFrame(timeVector, tags, values)}, nil -} - -// nolint:gocyclo -func processMetrics(esAgg *simplejson.Json, target *Query, query *backend.DataResponse, - props map[string]string) error { - frames := data.Frames{} - esAggBuckets := esAgg.Get("buckets").MustArray() - - jsonBuckets := make([]*simplejson.Json, len(esAggBuckets)) - - for i, v := range esAggBuckets { - jsonBuckets[i] = simplejson.NewFromAny(v) - } - - for _, metric := range target.Metrics { - if metric.Hide { - continue - } - - switch metric.Type { - case countType: - countFrames, err := processCountMetric(jsonBuckets, props) - if err != nil { - return fmt.Errorf("error processing count metric: %w", err) - } - frames = append(frames, countFrames...) - case percentilesType: - percentileFrames, err := processPercentilesMetric(metric, jsonBuckets, props) - if err != nil { - return fmt.Errorf("error processing percentiles metric: %w", err) - } - frames = append(frames, percentileFrames...) - case topMetricsType: - topMetricsFrames, err := processTopMetricsMetric(metric, jsonBuckets, props) - if err != nil { - return fmt.Errorf("error processing top metrics metric: %w", err) - } - frames = append(frames, topMetricsFrames...) - case extendedStatsType: - extendedStatsFrames, err := processExtendedStatsMetric(metric, jsonBuckets, props) - if err != nil { - return fmt.Errorf("error processing extended stats metric: %w", err) - } - - frames = append(frames, extendedStatsFrames...) - default: - defaultFrames, err := processDefaultMetric(metric, jsonBuckets, props) - if err != nil { - return fmt.Errorf("error processing default metric: %w", err) - } - frames = append(frames, defaultFrames...) - } - } - if query.Frames != nil { - oldFrames := query.Frames - frames = append(oldFrames, frames...) - } - query.Frames = frames - return nil -} - -// ensurePropFields guarantees all property columns exist even if prior frames lacked them -func ensurePropFields(fields *[]*data.Field, keys []string) { - have := map[string]bool{} - for _, f := range *fields { - have[f.Name] = true - } - for _, k := range keys { - if !have[k] { - d := "" - f := extractDataField(k, &d) - *fields = append(*fields, f) - } - } -} - -// appendPropsRow appends one row of property values; skipKey avoids double-append -func appendPropsRow(fields *[]*data.Field, props map[string]string, propKeys []string, skipKey string) { - for _, f := range *fields { - for _, pk := range propKeys { - if pk == skipKey { - continue - } - if f.Name == pk { - val := props[pk] - f.Append(&val) - } - } - } -} - -// appendMetrics appends all metric values for a single bucket/row -func appendMetrics(fields *[]*data.Field, bucket *simplejson.Json, target *Query) { - var values []interface{} - for _, metric := range target.Metrics { - switch metric.Type { - case countType: - addMetricValueToFields(fields, values, getMetricName(metric.Type), castToFloat(bucket.Get("doc_count"))) - case extendedStatsType: - addExtendedStatsToFields(fields, bucket, metric, values) - case percentilesType: - addPercentilesToFields(fields, bucket, metric, values) - case topMetricsType: - addTopMetricsToFields(fields, bucket, metric, values) - default: - addOtherMetricsToFields(fields, bucket, metric, values, target) - } - } -} - -// appendKeyColumnString appends a string key to an existing field or creates it -func appendKeyColumnString(fields *[]*data.Field, fieldName, key string) { - for _, f := range *fields { - if f.Name == fieldName { - k := key - f.Append(&k) - return - } - } - k := key - f := extractDataField(fieldName, &k) - f.Append(&k) - *fields = append(*fields, f) -} - -// appendBucketKeyValue appends the bucket's "key" (string or number) to fieldName -func appendBucketKeyValue(fields *[]*data.Field, fieldName string, bucket *simplejson.Json) error { - for _, f := range *fields { - if f.Name == fieldName { - if s, err := bucket.Get("key").String(); err == nil { - f.Append(&s) - return nil - } - num, err := bucket.Get("key").Float64() - if err != nil { - return fmt.Errorf("error appending bucket key to existing field %q: %w", fieldName, err) - } - f.Append(&num) - return nil - } - } - - // field not present yet - if s, err := bucket.Get("key").String(); err == nil { - f := extractDataField(fieldName, &s) - f.Append(&s) - *fields = append(*fields, f) - return nil - } - - num, err := bucket.Get("key").Float64() - if err != nil { - return fmt.Errorf("error appending bucket key to new field %q: %w", fieldName, err) - } - - f := extractDataField(fieldName, &num) - f.Append(&num) - *fields = append(*fields, f) - - return nil -} - -func processAggregationDocs( - esAgg *simplejson.Json, - aggDef *BucketAgg, - target *Query, - queryResult *backend.DataResponse, - props map[string]string, -) error { - propKeys := createPropKeys(props) - buckets := esAgg.Get("buckets") - - if arr := buckets.MustArray(); len(arr) > 0 { - fields := createFields(queryResult.Frames, propKeys) - ensurePropFields(&fields, propKeys) - - for _, v := range arr { - bucket := simplejson.NewFromAny(v) - - appendPropsRow(&fields, props, propKeys, "") - if aggDef.Field != "" { - if err := appendBucketKeyValue(&fields, aggDef.Field, bucket); err != nil { - return err - } - } - appendMetrics(&fields, bucket, target) - } - - queryResult.Frames = data.Frames{&data.Frame{Fields: fields}} - return nil - } - - if m := buckets.MustMap(); len(m) > 0 { - // default key column to "filter" for leaf filters - keyFieldName := aggDef.Field - if keyFieldName == "" { - keyFieldName = "filter" - } - - // ensure "filter" exists among props - hasFilter := false - for _, pk := range propKeys { - if pk == "filter" { - hasFilter = true - break - } - } - if !hasFilter { - propKeys = append(propKeys, "filter") - } - - fields := createFields(queryResult.Frames, propKeys) - ensurePropFields(&fields, propKeys) - - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - bucket := simplejson.NewFromAny(m[k]) - - locProps := make(map[string]string, len(props)+1) - for kk, vv := range props { - locProps[kk] = vv - } - locProps["filter"] = k - - // avoid double-append when the key column is "filter" - skip := "" - if keyFieldName == "filter" { - skip = "filter" - } - - appendPropsRow(&fields, locProps, propKeys, skip) - appendKeyColumnString(&fields, keyFieldName, k) - appendMetrics(&fields, bucket, target) - } - - queryResult.Frames = data.Frames{&data.Frame{Fields: fields}} - return nil - } - - // no buckets present - queryResult.Frames = data.Frames{} - return nil -} - -func extractDataField(name string, v interface{}) *data.Field { - var field *data.Field - switch v.(type) { - case *string: - field = data.NewField(name, nil, []*string{}) - case *float64: - field = data.NewField(name, nil, []*float64{}) - default: - field = &data.Field{} - } - isFilterable := true - field.Config = &data.FieldConfig{Filterable: &isFilterable} - return field -} - -func trimDatapoints(queryResult backend.DataResponse, target *Query) { - var histogram *BucketAgg - for _, bucketAgg := range target.BucketAggs { - if bucketAgg.Type == dateHistType { - histogram = bucketAgg - break - } - } - - if histogram == nil { - return - } - - trimEdges, err := castToInt(histogram.Settings.Get("trimEdges")) - if err != nil { - return - } - - frames := queryResult.Frames - - for _, frame := range frames { - for _, field := range frame.Fields { - if field.Len() > trimEdges*2 { - // first we delete the first "trim" items - for i := 0; i < trimEdges; i++ { - field.Delete(0) - } - - // then we delete the last "trim" items - for i := 0; i < trimEdges; i++ { - field.Delete(field.Len() - 1) - } - } - } - } -} - -// we sort the label's pairs by the label-key, -// and return the label-values -func getSortedLabelValues(labels data.Labels) []string { - keys := make([]string, 0, len(labels)) - for key := range labels { - keys = append(keys, key) - } - - sort.Strings(keys) - - values := make([]string, len(keys)) - for i, key := range keys { - values[i] = labels[key] - } - - return values -} - -func nameFields(queryResult backend.DataResponse, target *Query, keepLabelsInResponse bool) { - set := make(map[string]struct{}) - frames := queryResult.Frames - for _, v := range frames { - for _, vv := range v.Fields { - if metricType, exists := vv.Labels["metric"]; exists { - if _, ok := set[metricType]; !ok { - set[metricType] = struct{}{} - } - } - } - } - metricTypeCount := len(set) - for _, frame := range frames { - if frame.Meta != nil && frame.Meta.Type == data.FrameTypeTimeSeriesMulti { - // if it is a time-series-multi, it means it has two columns, one is "time", - // another is "number" - valueField := frame.Fields[1] - fieldName := getFieldName(*valueField, target, metricTypeCount) - // If we need to keep the labels in the response, to prevent duplication in names and to keep - // backward compatibility with alerting and expressions we use DisplayNameFromDS - if keepLabelsInResponse { - if valueField.Config == nil { - valueField.Config = &data.FieldConfig{} - } - valueField.Config.DisplayNameFromDS = fieldName - // If we don't need to keep labels (how frontend mode worked), we use frame.Name and remove labels - } else { - valueField.Labels = nil - frame.Name = fieldName - } - } - } -} - -func getFieldName(dataField data.Field, target *Query, metricTypeCount int) string { - metricType := dataField.Labels["metric"] - metricName := getMetricName(metricType) - delete(dataField.Labels, "metric") - - field := "" - if v, ok := dataField.Labels["field"]; ok { - field = v - delete(dataField.Labels, "field") - } - - if target.Alias != "" { - frameName := target.Alias - - subMatches := aliasPatternRegex.FindAllStringSubmatch(target.Alias, -1) - for _, subMatch := range subMatches { - group := subMatch[0] - - if len(subMatch) > 1 { - group = subMatch[1] - } - - if strings.Index(group, "term ") == 0 { - frameName = strings.Replace(frameName, subMatch[0], dataField.Labels[group[5:]], 1) - } - if v, ok := dataField.Labels[group]; ok { - frameName = strings.Replace(frameName, subMatch[0], v, 1) - } - if group == "metric" { - frameName = strings.Replace(frameName, subMatch[0], metricName, 1) - } - if group == "field" { - frameName = strings.Replace(frameName, subMatch[0], field, 1) - } - } - - return frameName - } - // todo, if field and pipelineAgg - if isPipelineAgg(metricType) { - if metricType != "" && isPipelineAggWithMultipleBucketPaths(metricType) { - metricID := "" - if v, ok := dataField.Labels["metricId"]; ok { - metricID = v - } - - for _, metric := range target.Metrics { - if metric.ID == metricID { - metricName = metric.Settings.Get("script").MustString() - for name, pipelineAgg := range metric.PipelineVariables { - for _, m := range target.Metrics { - if m.ID == pipelineAgg { - metricName = strings.ReplaceAll(metricName, "params."+name, describeMetric(m.Type, m.Field)) - } - } - } - } - } - } else { - if field != "" { - found := false - for _, metric := range target.Metrics { - if metric.ID == field { - metricName += " " + describeMetric(metric.Type, metric.Field) - found = true - } - } - if !found { - metricName = "Unset" - } - } - } - } else if field != "" { - metricName += " " + field - } - - delete(dataField.Labels, "metricId") - - if len(dataField.Labels) == 0 { - return metricName - } - - name := "" - for _, v := range getSortedLabelValues(dataField.Labels) { - name += v + " " - } - - if metricTypeCount == 1 { - return strings.TrimSpace(name) - } - - return strings.TrimSpace(name) + " " + metricName -} - -func getMetricName(metric string) string { - if text, ok := metricAggType[metric]; ok { - return text - } - - if text, ok := extendedStats[metric]; ok { - return text - } - - return metric -} - -func castToInt(j *simplejson.Json) (int, error) { - i, err := j.Int() - if err == nil { - return i, nil - } - - s, err := j.String() - if err != nil { - return 0, err - } - - v, err := strconv.Atoi(s) - if err != nil { - return 0, err - } - - return v, nil -} - -func castToFloat(j *simplejson.Json) *float64 { - f, err := j.Float64() - if err == nil { - return &f - } - - if s, err := j.String(); err == nil { - if strings.ToLower(s) == "nan" { - return nil - } - - if v, err := strconv.ParseFloat(s, 64); err == nil { - return &v - } - } - - return nil -} - -func getAsTime(j *simplejson.Json) (time.Time, error) { - // these are stored as numbers - number, err := j.Float64() - if err != nil { - return time.Time{}, err - } - - return time.UnixMilli(int64(number)).UTC(), nil -} - -func findAgg(target *Query, aggID string) (*BucketAgg, error) { - for _, v := range target.BucketAggs { - if aggID == v.ID { - return v, nil - } - } - return nil, errors.New("can't found aggDef, aggID:" + aggID) -} - +// getErrorFromElasticResponse extracts error message from Elasticsearch error response func getErrorFromElasticResponse(response *es.SearchResponse) string { var errorString string json := simplejson.NewFromAny(response.Error) @@ -1225,83 +152,7 @@ func getErrorFromElasticResponse(response *es.SearchResponse) string { return errorString } -// flatten flattens multi-level objects to single level objects. It uses dot notation to join keys. -func flatten(target map[string]interface{}, maxDepth int) map[string]interface{} { - // On frontend maxDepth wasn't used but as we are processing on backend - // let's put a limit to avoid infinite loop. 10 was chosen arbitrary. - output := make(map[string]interface{}) - step(0, maxDepth, target, "", output) - return output -} - -func step(currentDepth, maxDepth int, target map[string]interface{}, prev string, output map[string]interface{}) { - nextDepth := currentDepth + 1 - for key, value := range target { - newKey := strings.Trim(prev+"."+key, ".") - - v, ok := value.(map[string]interface{}) - if ok && len(v) > 0 && currentDepth < maxDepth { - step(nextDepth, maxDepth, v, newKey, output) - } else { - output[newKey] = value - } - } -} - -// sortPropNames orders propNames so that timeField is first (if it exists), log message field is second -// if shouldSortLogMessageField is true, and rest of propNames are ordered alphabetically -func sortPropNames(propNames map[string]bool, configuredFields es.ConfiguredFields, shouldSortLogMessageField bool) []string { - hasTimeField := false - hasLogMessageField := false - - var sortedPropNames []string - for k := range propNames { - if configuredFields.TimeField != "" && k == configuredFields.TimeField { - hasTimeField = true - } else if shouldSortLogMessageField && configuredFields.LogMessageField != "" && k == configuredFields.LogMessageField { - hasLogMessageField = true - } else { - sortedPropNames = append(sortedPropNames, k) - } - } - - sort.Strings(sortedPropNames) - - if hasLogMessageField { - sortedPropNames = append([]string{configuredFields.LogMessageField}, sortedPropNames...) - } - - if hasTimeField { - sortedPropNames = append([]string{configuredFields.TimeField}, sortedPropNames...) - } - - return sortedPropNames -} - -// findTheFirstNonNilDocValueForPropName finds the first non-nil value for propName in docs. If none of the values are non-nil, it returns the value of propName in the first doc. -func findTheFirstNonNilDocValueForPropName(docs []map[string]interface{}, propName string) interface{} { - for _, doc := range docs { - if doc[propName] != nil { - return doc[propName] - } - } - return docs[0][propName] -} - -func createFieldOfType[T int | float64 | bool | string](docs []map[string]interface{}, propName string, size int, isFilterable bool) *data.Field { - fieldVector := make([]*T, size) - for i, doc := range docs { - value, ok := doc[propName].(T) - if !ok { - continue - } - fieldVector[i] = &value - } - field := data.NewField(propName, nil, fieldVector) - field.Config = &data.FieldConfig{Filterable: &isFilterable} - return field -} - +// setPreferredVisType sets the preferred visualization type for a frame func setPreferredVisType(frame *data.Frame, visType data.VisType) { if frame.Meta == nil { frame.Meta = &data.FrameMeta{} @@ -1309,176 +160,3 @@ func setPreferredVisType(frame *data.Frame, visType data.VisType) { frame.Meta.PreferredVisualization = visType } - -func setLogsCustomMeta(frame *data.Frame, searchWords map[string]bool, limit int, total int) { - i := 0 - searchWordsList := make([]string, len(searchWords)) - for searchWord := range searchWords { - searchWordsList[i] = searchWord - i++ - } - sort.Strings(searchWordsList) - - if frame.Meta == nil { - frame.Meta = &data.FrameMeta{} - } - - if frame.Meta.Custom == nil { - frame.Meta.Custom = map[string]interface{}{} - } - - frame.Meta.Custom = map[string]interface{}{ - "searchWords": searchWordsList, - "limit": limit, - "total": total, - } -} - -func createFields(frames data.Frames, propKeys []string) []*data.Field { - var fields []*data.Field - have := map[string]bool{} - - // collect existing fields - for _, frame := range frames { - for _, f := range frame.Fields { - fields = append(fields, f) - have[f.Name] = true - } - } - - // add missing prop fields - for _, pk := range propKeys { - if !have[pk] { - fields = append(fields, data.NewField(pk, nil, []*string{})) - } - } - - return fields -} - -func getSortedKeys(data map[string]interface{}) []string { - keys := make([]string, 0, len(data)) - - for k := range data { - keys = append(keys, k) - } - - sort.Strings(keys) - return keys -} - -func createPropKeys(props map[string]string) []string { - propKeys := make([]string, 0) - for k := range props { - propKeys = append(propKeys, k) - } - sort.Strings(propKeys) - return propKeys -} - -func addMetricValueToFields(fields *[]*data.Field, values []interface{}, metricName string, value *float64) { - index := -1 - for i, f := range *fields { - if f.Name == metricName { - index = i - break - } - } - - var field data.Field - if index == -1 { - field = *data.NewField(metricName, nil, []*float64{}) - *fields = append(*fields, &field) - } else { - field = *(*fields)[index] - } - field.Append(value) -} - -func addPercentilesToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}) { - percentiles := bucket.GetPath(metric.ID, "values") - for _, percentileName := range getSortedKeys(percentiles.MustMap()) { - percentileValue := percentiles.Get(percentileName).MustFloat64() - addMetricValueToFields(fields, values, fmt.Sprintf("p%v %v", percentileName, metric.Field), &percentileValue) - } -} - -func addExtendedStatsToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}) { - metaKeys := make([]string, 0) - meta := metric.Meta.MustMap() - for k := range meta { - metaKeys = append(metaKeys, k) - } - sort.Strings(metaKeys) - for _, statName := range metaKeys { - v := meta[statName] - if enabled, ok := v.(bool); !ok || !enabled { - continue - } - var value *float64 - switch statName { - case "std_deviation_bounds_upper": - value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "upper")) - case "std_deviation_bounds_lower": - value = castToFloat(bucket.GetPath(metric.ID, "std_deviation_bounds", "lower")) - default: - value = castToFloat(bucket.GetPath(metric.ID, statName)) - } - - addMetricValueToFields(fields, values, getMetricName(metric.Type), value) - break - } -} - -func addTopMetricsToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}) { - baseName := getMetricName(metric.Type) - metrics := metric.Settings.Get("metrics").MustStringArray() - for _, metricField := range metrics { - // If we selected more than one metric we also add each metric name - metricName := baseName - if len(metrics) > 1 { - metricName += " " + metricField - } - top := bucket.GetPath(metric.ID, "top").MustArray() - metrics, hasMetrics := top[0].(map[string]interface{})["metrics"] - if hasMetrics { - metrics := metrics.(map[string]interface{}) - metricValue, hasMetricValue := metrics[metricField] - if hasMetricValue && metricValue != nil { - v := metricValue.(float64) - addMetricValueToFields(fields, values, metricName, &v) - } - } - } -} - -func addOtherMetricsToFields(fields *[]*data.Field, bucket *simplejson.Json, metric *MetricAgg, values []interface{}, target *Query) { - metricName := getMetricName(metric.Type) - otherMetrics := make([]*MetricAgg, 0) - - for _, m := range target.Metrics { - // To other metrics we add metric of the same type that are not the current metric - if m.ID != metric.ID && m.Type == metric.Type { - otherMetrics = append(otherMetrics, m) - } - } - - if len(otherMetrics) > 0 { - metricName += " " + metric.Field - - // We check if we have metric with the same type and same field name - // If so, append metric.ID to the metric name - for _, m := range otherMetrics { - if m.Field == metric.Field { - metricName += " " + metric.ID - break - } - } - - if metric.Type == "bucket_script" { - // Use the formula in the column name - metricName = metric.Settings.Get("script").MustString("") - } - } - addMetricValueToFields(fields, values, metricName, castToFloat(bucket.GetPath(metric.ID, "value"))) -} diff --git a/pkg/tsdb/elasticsearch/response_utils.go b/pkg/tsdb/elasticsearch/response_utils.go new file mode 100644 index 00000000000..c101633d0c2 --- /dev/null +++ b/pkg/tsdb/elasticsearch/response_utils.go @@ -0,0 +1,127 @@ +package elasticsearch + +import ( + "errors" + "sort" + "strconv" + "strings" + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" + es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" +) + +// flatten flattens multi-level objects to single level objects. It uses dot notation to join keys. +func flatten(target map[string]interface{}, maxDepth int) map[string]interface{} { + // On frontend maxDepth wasn't used but as we are processing on backend + // let's put a limit to avoid infinite loop. 10 was chosen arbitrary. + output := make(map[string]interface{}) + step(0, maxDepth, target, "", output) + return output +} + +// step is a recursive helper for flatten +func step(currentDepth, maxDepth int, target map[string]interface{}, prev string, output map[string]interface{}) { + nextDepth := currentDepth + 1 + for key, value := range target { + newKey := strings.Trim(prev+"."+key, ".") + + v, ok := value.(map[string]interface{}) + if ok && len(v) > 0 && currentDepth < maxDepth { + step(nextDepth, maxDepth, v, newKey, output) + } else { + output[newKey] = value + } + } +} + +// sortPropNames orders propNames so that timeField is first (if it exists), log message field is second +// if shouldSortLogMessageField is true, and rest of propNames are ordered alphabetically +func sortPropNames(propNames map[string]bool, configuredFields es.ConfiguredFields, shouldSortLogMessageField bool) []string { + hasTimeField := false + hasLogMessageField := false + + var sortedPropNames []string + for k := range propNames { + if configuredFields.TimeField != "" && k == configuredFields.TimeField { + hasTimeField = true + } else if shouldSortLogMessageField && configuredFields.LogMessageField != "" && k == configuredFields.LogMessageField { + hasLogMessageField = true + } else { + sortedPropNames = append(sortedPropNames, k) + } + } + + sort.Strings(sortedPropNames) + + if hasLogMessageField { + sortedPropNames = append([]string{configuredFields.LogMessageField}, sortedPropNames...) + } + + if hasTimeField { + sortedPropNames = append([]string{configuredFields.TimeField}, sortedPropNames...) + } + + return sortedPropNames +} + +// castToInt casts a simplejson.Json value to int +func castToInt(j *simplejson.Json) (int, error) { + i, err := j.Int() + if err == nil { + return i, nil + } + + s, err := j.String() + if err != nil { + return 0, err + } + + v, err := strconv.Atoi(s) + if err != nil { + return 0, err + } + + return v, nil +} + +// castToFloat casts a simplejson.Json value to float64 +func castToFloat(j *simplejson.Json) *float64 { + f, err := j.Float64() + if err == nil { + return &f + } + + if s, err := j.String(); err == nil { + if strings.ToLower(s) == "nan" { + return nil + } + + if v, err := strconv.ParseFloat(s, 64); err == nil { + return &v + } + } + + return nil +} + +// getAsTime converts a simplejson.Json value to time.Time +func getAsTime(j *simplejson.Json) (time.Time, error) { + // these are stored as numbers + number, err := j.Float64() + if err != nil { + return time.Time{}, err + } + + return time.UnixMilli(int64(number)).UTC(), nil +} + +// findAgg finds a bucket aggregation by ID +func findAgg(target *Query, aggID string) (*BucketAgg, error) { + for _, v := range target.BucketAggs { + if aggID == v.ID { + return v, nil + } + } + return nil, errors.New("can't found aggDef, aggID:" + aggID) +} diff --git a/pkg/tsdb/mssql/azure/connection.go b/pkg/tsdb/mssql/azure/connection.go new file mode 100644 index 00000000000..40978229b39 --- /dev/null +++ b/pkg/tsdb/mssql/azure/connection.go @@ -0,0 +1,40 @@ +package azure + +import ( + "fmt" + + "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" +) + +func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool) (string, error) { + connStr := "" + switch c := azureCredentials.(type) { + case *azcredentials.AzureManagedIdentityCredentials: + if azureManagedIdentityClientId != "" { + connStr += fmt.Sprintf("user id=%s;", azureManagedIdentityClientId) + } + connStr += fmt.Sprintf("fedauth=%s;", + "ActiveDirectoryManagedIdentity") + case *azcredentials.AzureClientSecretCredentials: + connStr += fmt.Sprintf("user id=%s@%s;password=%s;fedauth=%s;", + c.ClientId, + c.TenantId, + c.ClientSecret, + "ActiveDirectoryApplication", + ) + case *azcredentials.AzureEntraPasswordCredentials: + if azureEntraPasswordCredentialsEnabled { + connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;", + c.UserId, + c.Password, + c.ClientId, + "ActiveDirectoryPassword", + ) + } else { + return "", fmt.Errorf("azure entra password authentication is not enabled") + } + default: + return "", fmt.Errorf("unsupported azure authentication type") + } + return connStr, nil +} diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index 3c617b5828a..ca8595edd2e 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -2,32 +2,18 @@ package mssql import ( "context" - "database/sql" "encoding/json" "fmt" - "net/url" - "reflect" - "regexp" - "strconv" - "strings" - "time" - "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" - mssql "github.com/microsoft/go-mssqldb" - "github.com/microsoft/go-mssqldb/azuread" _ "github.com/microsoft/go-mssqldb/integratedauth/krb5" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/mssql/kerberos" "github.com/grafana/grafana/pkg/tsdb/mssql/sqleng" - "github.com/grafana/grafana/pkg/tsdb/mssql/utils" - "github.com/grafana/grafana/pkg/util" ) type Service struct { @@ -35,16 +21,6 @@ type Service struct { logger log.Logger } -const ( - azureAuthentication = "Azure AD Authentication" - windowsAuthentication = "Windows Authentication" - sqlServerAuthentication = "SQL Server Authentication" - kerberosRaw = "Windows AD: Username + password" - kerberosKeytab = "Windows AD: Keytab" - kerberosCredentialCache = "Windows AD: Credential cache" // #nosec G101 - kerberosCredentialCacheFile = "Windows AD: Credential cache file" // #nosec G101 -) - func ProvideService(cfg *setting.Cfg) *Service { logger := backend.NewLoggerWith("logger", "tsdb.mssql") return &Service{ @@ -70,72 +46,6 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return dsHandler.QueryData(ctx, req) } -func newMSSQL(ctx context.Context, driverName string, userFacingDefaultError string, rowLimit int64, dsInfo sqleng.DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, *sqleng.DataSourceHandler, error) { - var connector *mssql.Connector - var err error - if driverName == "azuresql" { - connector, err = azuread.NewConnector(cnnstr) - } else { - connector, err = mssql.NewConnector(cnnstr) - } - - if err != nil { - logger.Error("mssql connector creation failed", "error", err) - return nil, nil, fmt.Errorf("mssql connector creation failed") - } - - proxyClient, err := settings.ProxyClient(ctx) - if err != nil { - logger.Error("mssql proxy creation failed", "error", err) - return nil, nil, fmt.Errorf("mssql proxy creation failed") - } - - if proxyClient.SecureSocksProxyEnabled() { - dialer, err := proxyClient.NewSecureSocksProxyContextDialer() - if err != nil { - logger.Error("mssql proxy creation failed", "error", err) - return nil, nil, fmt.Errorf("mssql proxy creation failed") - } - URL, err := ParseURL(dsInfo.URL, logger) - if err != nil { - return nil, nil, err - } - - mssqlDialer, err := newMSSQLProxyDialer(URL.Hostname(), dialer) - if err != nil { - return nil, nil, err - } - // update the mssql dialer with the proxy dialer - connector.Dialer = (mssqlDialer) - } - - config := sqleng.DataPluginConfiguration{ - DSInfo: dsInfo, - MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"}, - RowLimit: rowLimit, - } - - queryResultTransformer := mssqlQueryResultTransformer{ - userError: userFacingDefaultError, - } - - db := sql.OpenDB(connector) - - db.SetMaxOpenConns(config.DSInfo.JsonData.MaxOpenConns) - db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) - db.SetConnMaxLifetime(time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second) - - handler, err := sqleng.NewQueryDataHandler(userFacingDefaultError, db, config, &queryResultTransformer, newMssqlMacroEngine(), - logger) - if err != nil { - logger.Error("Failed connecting to Postgres", "err", err) - return nil, nil, err - } - - logger.Debug("Successfully connected to Postgres") - return db, handler, nil -} - func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { grafCfg := backend.GrafanaConfigFromContext(ctx) @@ -151,14 +61,11 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc ConnectionTimeout: 0, SecureDSProxy: false, } - azureCredentials, err := utils.GetAzureCredentials(settings) - if err != nil { - return nil, fmt.Errorf("error reading azure credentials") - } - kerberosAuth, err := kerberos.GetKerberosSettings(settings) + azureSettings, err := azsettings.ReadSettings(ctx) if err != nil { - return nil, fmt.Errorf("error getting kerberos settings: %w", err) + logger.Error("failed to read Azure settings from Grafana", "error", err.Error()) + return nil, err } err = json.Unmarshal(settings.JSONData, &jsonData) @@ -181,23 +88,18 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc UID: settings.UID, DecryptedSecureJSONData: settings.DecryptedSecureJSONData, } - cnnstr, err := generateConnectionString(dsInfo, cfg.Azure.ManagedIdentityClientId, cfg.Azure.AzureEntraPasswordCredentialsEnabled, azureCredentials, kerberosAuth, logger) - if err != nil { - return nil, err - } - - driverName := "mssql" - if jsonData.AuthenticationType == azureAuthentication { - driverName = "azuresql" - } userFacingDefaultError, err := grafCfg.UserFacingDefaultError() if err != nil { return nil, err } - _, handler, err := newMSSQL(ctx, driverName, userFacingDefaultError, sqlCfg.RowLimit, dsInfo, cnnstr, logger, settings) - + config := sqleng.DataPluginConfiguration{ + DSInfo: dsInfo, + MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"}, + RowLimit: sqlCfg.RowLimit, + } + handler, err := sqleng.NewQueryDataHandler(ctx, settings, userFacingDefaultError, config, logger, azureSettings) if err != nil { logger.Error("Failed connecting to MSSQL", "err", err) return nil, err @@ -208,159 +110,6 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc } } -// ParseURL is called also from pkg/api/datasource/validation.go, -// which uses a different logging interface, -// so we have a special minimal interface that is fulfilled by -// both places. -type DebugOnlyLogger interface { - Debug(msg string, args ...interface{}) -} - -// ParseURL tries to parse an MSSQL URL string into a URL object. -func ParseURL(u string, logger DebugOnlyLogger) (*url.URL, error) { - logger.Debug("Parsing MSSQL URL", "url", u) - - // Recognize ODBC connection strings like host\instance:1234 - reODBC := regexp.MustCompile(`^[^\\:]+(?:\\[^:]+)?(?::\d+)?(?:;.+)?$`) - var host string - switch { - case reODBC.MatchString(u): - logger.Debug("Recognized as ODBC URL format", "url", u) - host = u - default: - logger.Debug("Couldn't recognize as valid MSSQL URL", "url", u) - return nil, fmt.Errorf("unrecognized MSSQL URL format: %q", u) - } - return &url.URL{ - Scheme: "sqlserver", - Host: host, - }, nil -} - -func generateConnectionString(dsInfo sqleng.DataSourceInfo, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) { - const dfltPort = "0" - var addr util.NetworkAddress - if dsInfo.URL != "" { - u, err := ParseURL(dsInfo.URL, logger) - if err != nil { - return "", err - } - addr, err = util.SplitHostPortDefault(u.Host, "localhost", dfltPort) - if err != nil { - return "", err - } - } else { - addr = util.NetworkAddress{ - Host: "localhost", - Port: dfltPort, - } - } - - args := []any{ - "url", dsInfo.URL, "host", addr.Host, - } - if addr.Port != "0" { - args = append(args, "port", addr.Port) - } - logger.Debug("Generating connection string", args...) - - encrypt := dsInfo.JsonData.Encrypt - tlsSkipVerify := dsInfo.JsonData.TlsSkipVerify - hostNameInCertificate := dsInfo.JsonData.Servername - certificate := dsInfo.JsonData.RootCertFile - connStr := fmt.Sprintf("server=%s;database=%s;", - addr.Host, - dsInfo.Database, - ) - - switch dsInfo.JsonData.AuthenticationType { - case azureAuthentication: - azureCredentialDSNFragment, err := getAzureCredentialDSNFragment(azureCredentials, azureManagedIdentityClientId, azureEntraPasswordCredentialsEnabled) - if err != nil { - return "", err - } - connStr += azureCredentialDSNFragment - case windowsAuthentication: - // No user id or password. We're using windows single sign on. - case kerberosRaw, kerberosKeytab, kerberosCredentialCacheFile, kerberosCredentialCache: - connStr = kerberos.Krb5ParseAuthCredentials(addr.Host, addr.Port, dsInfo.Database, dsInfo.User, dsInfo.DecryptedSecureJSONData["password"], kerberosAuth) - default: - connStr += fmt.Sprintf("user id=%s;password=%s;", dsInfo.User, dsInfo.DecryptedSecureJSONData["password"]) - } - - // Port number 0 means to determine the port automatically, so we can let the driver choose - if addr.Port != "0" { - connStr += fmt.Sprintf("port=%s;", addr.Port) - } - switch encrypt { - case "true": - connStr += fmt.Sprintf("encrypt=%s;TrustServerCertificate=%t;", encrypt, tlsSkipVerify) - if hostNameInCertificate != "" { - connStr += fmt.Sprintf("hostNameInCertificate=%s;", hostNameInCertificate) - } - - if certificate != "" { - connStr += fmt.Sprintf("certificate=%s;", certificate) - } - case "disable": - connStr += fmt.Sprintf("encrypt=%s;", dsInfo.JsonData.Encrypt) - } - - if dsInfo.JsonData.ConnectionTimeout != 0 { - connStr += fmt.Sprintf("connection timeout=%d;", dsInfo.JsonData.ConnectionTimeout) - } - - return connStr, nil -} - -func getAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool) (string, error) { - connStr := "" - switch c := azureCredentials.(type) { - case *azcredentials.AzureManagedIdentityCredentials: - if azureManagedIdentityClientId != "" { - connStr += fmt.Sprintf("user id=%s;", azureManagedIdentityClientId) - } - connStr += fmt.Sprintf("fedauth=%s;", - "ActiveDirectoryManagedIdentity") - case *azcredentials.AzureClientSecretCredentials: - connStr += fmt.Sprintf("user id=%s@%s;password=%s;fedauth=%s;", - c.ClientId, - c.TenantId, - c.ClientSecret, - "ActiveDirectoryApplication", - ) - case *azcredentials.AzureEntraPasswordCredentials: - if azureEntraPasswordCredentialsEnabled { - connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;", - c.UserId, - c.Password, - c.ClientId, - "ActiveDirectoryPassword", - ) - } else { - return "", fmt.Errorf("azure entra password authentication is not enabled") - } - default: - return "", fmt.Errorf("unsupported azure authentication type") - } - return connStr, nil -} - -type mssqlQueryResultTransformer struct { - userError string -} - -func (t *mssqlQueryResultTransformer) TransformQueryError(logger log.Logger, err error) error { - // go-mssql overrides source error, so we currently match on string - // ref https://github.com/denisenkom/go-mssqldb/blob/045585d74f9069afe2e115b6235eb043c8047043/tds.go#L904 - if strings.HasPrefix(strings.ToLower(err.Error()), "unable to open tcp connection with host") { - logger.Error("Query error", "error", err) - return fmt.Errorf("failed to connect to server - %s", t.userError) - } - - return err -} - // CheckHealth pings the connected SQL database func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { dsHandler, err := s.getDataSourceHandler(ctx, req.PluginContext) @@ -370,100 +119,3 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque return dsHandler.CheckHealth(ctx, req) } - -func (t *mssqlQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { - return []sqlutil.StringConverter{ - { - Name: "handle MONEY", - InputScanKind: reflect.Slice, - InputTypeName: "MONEY", - ConversionFunc: func(in *string) (*string, error) { return in, nil }, - Replacer: &sqlutil.StringFieldReplacer{ - OutputFieldType: data.FieldTypeNullableFloat64, - ReplaceFunc: func(in *string) (any, error) { - if in == nil { - return nil, nil - } - v, err := strconv.ParseFloat(*in, 64) - if err != nil { - return nil, err - } - return &v, nil - }, - }, - }, - { - Name: "handle SMALLMONEY", - InputScanKind: reflect.Slice, - InputTypeName: "SMALLMONEY", - ConversionFunc: func(in *string) (*string, error) { return in, nil }, - Replacer: &sqlutil.StringFieldReplacer{ - OutputFieldType: data.FieldTypeNullableFloat64, - ReplaceFunc: func(in *string) (any, error) { - if in == nil { - return nil, nil - } - v, err := strconv.ParseFloat(*in, 64) - if err != nil { - return nil, err - } - return &v, nil - }, - }, - }, - { - Name: "handle DECIMAL", - InputScanKind: reflect.Slice, - InputTypeName: "DECIMAL", - ConversionFunc: func(in *string) (*string, error) { return in, nil }, - Replacer: &sqlutil.StringFieldReplacer{ - OutputFieldType: data.FieldTypeNullableFloat64, - ReplaceFunc: func(in *string) (any, error) { - if in == nil { - return nil, nil - } - v, err := strconv.ParseFloat(*in, 64) - if err != nil { - return nil, err - } - return &v, nil - }, - }, - }, - { - Name: "handle UNIQUEIDENTIFIER", - InputScanKind: reflect.Slice, - InputTypeName: "UNIQUEIDENTIFIER", - ConversionFunc: func(in *string) (*string, error) { return in, nil }, - Replacer: &sqlutil.StringFieldReplacer{ - OutputFieldType: data.FieldTypeNullableString, - ReplaceFunc: func(in *string) (any, error) { - if in == nil { - return nil, nil - } - uuid := &mssql.UniqueIdentifier{} - if err := uuid.Scan([]byte(*in)); err != nil { - return nil, err - } - v := uuid.String() - return &v, nil - }, - }, - }, - { - Name: "handle SQL_VARIANT", - InputScanKind: reflect.Pointer, - InputTypeName: "SQL_VARIANT", - ConversionFunc: func(in *string) (*string, error) { return in, nil }, - Replacer: &sqlutil.StringFieldReplacer{ - OutputFieldType: data.FieldTypeNullableString, - ReplaceFunc: func(in *string) (any, error) { - if in == nil { - return nil, nil - } - return in, nil - }, - }, - }, - } -} diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index c81b48d274d..7368fd5d93b 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -3,21 +3,21 @@ package mssql import ( "context" "database/sql" - "encoding/json" "fmt" "math/rand" "os" "testing" "time" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/tsdb/mssql/kerberos" "github.com/grafana/grafana/pkg/tsdb/mssql/sqleng" + "github.com/grafana/grafana/pkg/tsdb/mssql/utils" ) // To run this test, set runMssqlTests=true @@ -39,7 +39,6 @@ func TestMSSQL(t *testing.T) { t.Skip() } - queryResultTransformer := mssqlQueryResultTransformer{} dsInfo := sqleng.DataSourceInfo{} config := sqleng.DataPluginConfiguration{ DSInfo: dsInfo, @@ -50,8 +49,10 @@ func TestMSSQL(t *testing.T) { logger := backend.NewLoggerWith("logger", "mssql.test") db := initMSSQLTestDB(t, config.DSInfo.JsonData) + ctx := context.Background() + settings := backend.DataSourceInstanceSettings{} - endpoint, err := sqleng.NewQueryDataHandler("", db, config, &queryResultTransformer, newMssqlMacroEngine(), logger) + endpoint, err := sqleng.NewQueryDataHandler(ctx, settings, "", config, logger, &azsettings.AzureSettings{}) require.NoError(t, err) fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) @@ -799,14 +800,16 @@ func TestMSSQL(t *testing.T) { require.NoError(t, err) t.Run("When doing a metric query using stored procedure should return correct result", func(t *testing.T) { - queryResultTransformer := mssqlQueryResultTransformer{} dsInfo := sqleng.DataSourceInfo{} config := sqleng.DataPluginConfiguration{ DSInfo: dsInfo, MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"}, RowLimit: 1000000, } - endpoint, err := sqleng.NewQueryDataHandler("", db, config, &queryResultTransformer, newMssqlMacroEngine(), logger) + ctx := context.Background() + settings := backend.DataSourceInstanceSettings{} + + endpoint, err := sqleng.NewQueryDataHandler(ctx, settings, "", config, logger, &azsettings.AzureSettings{}) require.NoError(t, err) query := &backend.QueryDataRequest{ Queries: []backend.DataQuery{ @@ -1202,7 +1205,6 @@ func TestMSSQL(t *testing.T) { }) t.Run("When row limit set to 1", func(t *testing.T) { - queryResultTransformer := mssqlQueryResultTransformer{} dsInfo := sqleng.DataSourceInfo{} config := sqleng.DataPluginConfiguration{ DSInfo: dsInfo, @@ -1210,7 +1212,10 @@ func TestMSSQL(t *testing.T) { RowLimit: 1, } - handler, err := sqleng.NewQueryDataHandler("", db, config, &queryResultTransformer, newMssqlMacroEngine(), logger) + ctx := context.Background() + settings := backend.DataSourceInstanceSettings{} + + handler, err := sqleng.NewQueryDataHandler(ctx, settings, "", config, logger, &azsettings.AzureSettings{}) require.NoError(t, err) t.Run("When doing a table query that returns 2 rows should limit the result to 1 row", func(t *testing.T) { @@ -1313,7 +1318,7 @@ func TestMSSQL(t *testing.T) { } func TestTransformQueryError(t *testing.T) { - transformer := &mssqlQueryResultTransformer{} + transformer := &utils.MSSQLQueryResultTransformer{} logger := backend.NewLoggerWith("logger", "mssql.test") @@ -1334,252 +1339,6 @@ func TestTransformQueryError(t *testing.T) { }) } -func TestGenerateConnectionString(t *testing.T) { - kerberosLookup := []kerberos.KerberosLookup{ - { - Address: "example.host", - DBName: "testDB", - User: "testUser", - CredentialCacheFilename: "/tmp/cache", - }, - } - tmpFile := genTempCacheFile(t, kerberosLookup) - defer func() { - err := os.Remove(tmpFile) - if err != nil { - t.Log(err) - } - }() - - testCases := []struct { - desc string - kerberosCfg kerberos.KerberosAuth - dataSource sqleng.DataSourceInfo - expConnStr string - }{ - { - desc: "Use Kerberos Credential Cache", - kerberosCfg: kerberos.KerberosAuth{ - CredentialCache: "/tmp/krb5cc_1000", - ConfigFilePath: "/etc/krb5.conf", - UDPConnectionLimit: 1, - }, - dataSource: sqleng.DataSourceInfo{ - URL: "localhost", - Database: "database", - JsonData: sqleng.JsonData{ - AuthenticationType: "Windows AD: Credential cache", - }, - }, - expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;krb5-credcachefile=/tmp/krb5cc_1000;", - }, - { - desc: "Use Kerberos Credential Cache File path", - kerberosCfg: kerberos.KerberosAuth{ - CredentialCacheLookupFile: tmpFile, - ConfigFilePath: "/etc/krb5.conf", - UDPConnectionLimit: 1, - }, - dataSource: sqleng.DataSourceInfo{ - URL: "example.host", - Database: "testDB", - User: "testUser", - JsonData: sqleng.JsonData{ - AuthenticationType: "Windows AD: Credential cache file", - }, - }, - expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=example.host;database=testDB;krb5-credcachefile=/tmp/cache;", - }, - { - desc: "Use Kerberos Keytab", - kerberosCfg: kerberos.KerberosAuth{ - KeytabFilePath: "/foo/bar.keytab", - ConfigFilePath: "/etc/krb5.conf", - UDPConnectionLimit: 1, - }, - dataSource: sqleng.DataSourceInfo{ - URL: "localhost", - Database: "database", - User: "foo@test.lab", - JsonData: sqleng.JsonData{ - AuthenticationType: "Windows AD: Keytab", - }, - }, - expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;krb5-keytabfile=/foo/bar.keytab;", - }, - { - desc: "Use Kerberos Username and Password", - kerberosCfg: kerberos.KerberosAuth{ - ConfigFilePath: "/etc/krb5.conf", - UDPConnectionLimit: 1, - }, - dataSource: sqleng.DataSourceInfo{ - URL: "localhost", - Database: "database", - User: "foo@test.lab", - DecryptedSecureJSONData: map[string]string{ - "password": "foo", - }, - JsonData: sqleng.JsonData{ - AuthenticationType: "Windows AD: Username + password", - }, - }, - expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;password=foo;", - }, - { - desc: "Use non-default UDP connection limit", - kerberosCfg: kerberos.KerberosAuth{ - ConfigFilePath: "/etc/krb5.conf", - UDPConnectionLimit: 0, - }, - dataSource: sqleng.DataSourceInfo{ - URL: "localhost", - Database: "database", - User: "foo@test.lab", - DecryptedSecureJSONData: map[string]string{ - "password": "foo", - }, - JsonData: sqleng.JsonData{ - AuthenticationType: "Windows AD: Username + password", - }, - }, - expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;password=foo;krb5-udppreferencelimit=0;", - }, - - { - desc: "From URL w/ port", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost:1001", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost;database=database;user id=user;password=;port=1001;", - }, - // When no port is specified, the driver should be allowed to choose - { - desc: "From URL w/o port", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost;database=database;user id=user;password=;", - }, - // Port 0 should be equivalent to not specifying a port, i.e. let the driver choose - { - desc: "From URL w port 0", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost:0", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost;database=database;user id=user;password=;", - }, - { - desc: "With instance name", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;database=database;user id=user;password=;", - }, - { - desc: "With instance name and port", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance:333", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;", - }, - { - desc: "With instance name and ApplicationIntent", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance;ApplicationIntent=ReadOnly", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;ApplicationIntent=ReadOnly;database=database;user id=user;password=;", - }, - { - desc: "With ApplicationIntent instance name and port", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance:333;ApplicationIntent=ReadOnly", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;ApplicationIntent=ReadOnly;", - }, - { - desc: "With instance name", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;database=database;user id=user;password=;", - }, - { - desc: "With instance name and port", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance:333", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;", - }, - { - desc: "With instance name and ApplicationIntent", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance;ApplicationIntent=ReadOnly", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;ApplicationIntent=ReadOnly;database=database;user id=user;password=;", - }, - { - desc: "With ApplicationIntent instance name and port", - dataSource: sqleng.DataSourceInfo{ - URL: "localhost\\instance:333;ApplicationIntent=ReadOnly", - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;ApplicationIntent=ReadOnly;", - }, - { - desc: "Defaults", - dataSource: sqleng.DataSourceInfo{ - Database: "database", - User: "user", - JsonData: sqleng.JsonData{}, - }, - expConnStr: "server=localhost;database=database;user id=user;password=;", - }, - } - - logger := backend.NewLoggerWith("logger", "mssql.test") - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - connStr, err := generateConnectionString(tc.dataSource, "", false, nil, tc.kerberosCfg, logger) - require.NoError(t, err) - assert.Equal(t, tc.expConnStr, connStr) - }) - } -} - func initMSSQLTestDB(t *testing.T, jsonData sqleng.JsonData) *sql.DB { t.Helper() @@ -1614,21 +1373,3 @@ func genTimeRangeByInterval(from time.Time, duration time.Duration, interval tim return timeRange } - -func genTempCacheFile(t *testing.T, lookups []kerberos.KerberosLookup) string { - content, err := json.Marshal(lookups) - if err != nil { - t.Fatalf("Unable to marshall json for temp lookup: %v", err) - } - - tmpFile, err := os.CreateTemp("", "lookup*.json") - if err != nil { - t.Fatalf("Unable to create temporary file for temp lookup: %v", err) - } - - if _, err := tmpFile.Write(content); err != nil { - t.Fatalf("Unable to write to temporary file for temp lookup: %v", err) - } - - return tmpFile.Name() -} diff --git a/pkg/tsdb/mssql/sqleng/connection.go b/pkg/tsdb/mssql/sqleng/connection.go new file mode 100644 index 00000000000..58a6d014219 --- /dev/null +++ b/pkg/tsdb/mssql/sqleng/connection.go @@ -0,0 +1,158 @@ +package sqleng + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/mssql/azure" + "github.com/grafana/grafana/pkg/tsdb/mssql/kerberos" + "github.com/grafana/grafana/pkg/tsdb/mssql/utils" + "github.com/grafana/grafana/pkg/util" + mssql "github.com/microsoft/go-mssqldb" + "github.com/microsoft/go-mssqldb/azuread" +) + +func newMSSQL(ctx context.Context, driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, error) { + var connector *mssql.Connector + var err error + if driverName == "azuresql" { + connector, err = azuread.NewConnector(cnnstr) + } else { + connector, err = mssql.NewConnector(cnnstr) + } + + if err != nil { + logger.Error("mssql connector creation failed", "error", err) + return nil, fmt.Errorf("mssql connector creation failed") + } + + proxyClient, err := settings.ProxyClient(ctx) + if err != nil { + logger.Error("mssql proxy creation failed", "error", err) + return nil, fmt.Errorf("mssql proxy creation failed") + } + + if proxyClient.SecureSocksProxyEnabled() { + dialer, err := proxyClient.NewSecureSocksProxyContextDialer() + if err != nil { + logger.Error("mssql proxy creation failed", "error", err) + return nil, fmt.Errorf("mssql proxy creation failed") + } + URL, err := utils.ParseURL(dsInfo.URL, logger) + if err != nil { + return nil, err + } + + mssqlDialer, err := newMSSQLProxyDialer(URL.Hostname(), dialer) + if err != nil { + return nil, err + } + // update the mssql dialer with the proxy dialer + connector.Dialer = (mssqlDialer) + } + + config := DataPluginConfiguration{ + DSInfo: dsInfo, + MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"}, + RowLimit: rowLimit, + } + + db := sql.OpenDB(connector) + + db.SetMaxOpenConns(config.DSInfo.JsonData.MaxOpenConns) + db.SetMaxIdleConns(config.DSInfo.JsonData.MaxIdleConns) + db.SetConnMaxLifetime(time.Duration(config.DSInfo.JsonData.ConnMaxLifetime) * time.Second) + + return db, nil +} + +const ( + azureAuthentication = "Azure AD Authentication" + windowsAuthentication = "Windows Authentication" + sqlServerAuthentication = "SQL Server Authentication" + kerberosRaw = "Windows AD: Username + password" + kerberosKeytab = "Windows AD: Keytab" + kerberosCredentialCache = "Windows AD: Credential cache" // #nosec G101 + kerberosCredentialCacheFile = "Windows AD: Credential cache file" // #nosec G101 +) + +func generateConnectionString(dsInfo DataSourceInfo, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) { + const dfltPort = "0" + var addr util.NetworkAddress + if dsInfo.URL != "" { + u, err := utils.ParseURL(dsInfo.URL, logger) + if err != nil { + return "", err + } + addr, err = util.SplitHostPortDefault(u.Host, "localhost", dfltPort) + if err != nil { + return "", err + } + } else { + addr = util.NetworkAddress{ + Host: "localhost", + Port: dfltPort, + } + } + + args := []any{ + "url", dsInfo.URL, "host", addr.Host, + } + if addr.Port != "0" { + args = append(args, "port", addr.Port) + } + logger.Debug("Generating connection string", args...) + + encrypt := dsInfo.JsonData.Encrypt + tlsSkipVerify := dsInfo.JsonData.TlsSkipVerify + hostNameInCertificate := dsInfo.JsonData.Servername + certificate := dsInfo.JsonData.RootCertFile + connStr := fmt.Sprintf("server=%s;database=%s;", + addr.Host, + dsInfo.Database, + ) + + switch dsInfo.JsonData.AuthenticationType { + case azureAuthentication: + azureCredentialDSNFragment, err := azure.GetAzureCredentialDSNFragment(azureCredentials, azureManagedIdentityClientId, azureEntraPasswordCredentialsEnabled) + if err != nil { + return "", err + } + connStr += azureCredentialDSNFragment + case windowsAuthentication: + // No user id or password. We're using windows single sign on. + case kerberosRaw, kerberosKeytab, kerberosCredentialCacheFile, kerberosCredentialCache: + connStr = kerberos.Krb5ParseAuthCredentials(addr.Host, addr.Port, dsInfo.Database, dsInfo.User, dsInfo.DecryptedSecureJSONData["password"], kerberosAuth) + default: + connStr += fmt.Sprintf("user id=%s;password=%s;", dsInfo.User, dsInfo.DecryptedSecureJSONData["password"]) + } + + // Port number 0 means to determine the port automatically, so we can let the driver choose + if addr.Port != "0" { + connStr += fmt.Sprintf("port=%s;", addr.Port) + } + switch encrypt { + case "true": + connStr += fmt.Sprintf("encrypt=%s;TrustServerCertificate=%t;", encrypt, tlsSkipVerify) + if hostNameInCertificate != "" { + connStr += fmt.Sprintf("hostNameInCertificate=%s;", hostNameInCertificate) + } + + if certificate != "" { + connStr += fmt.Sprintf("certificate=%s;", certificate) + } + case "disable": + connStr += fmt.Sprintf("encrypt=%s;", dsInfo.JsonData.Encrypt) + } + + if dsInfo.JsonData.ConnectionTimeout != 0 { + connStr += fmt.Sprintf("connection timeout=%d;", dsInfo.JsonData.ConnectionTimeout) + } + + return connStr, nil +} diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/sqleng/macros.go similarity index 92% rename from pkg/tsdb/mssql/macros.go rename to pkg/tsdb/mssql/sqleng/macros.go index de01cc9a9f2..c0ab99cbd77 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/sqleng/macros.go @@ -1,4 +1,4 @@ -package mssql +package sqleng import ( "fmt" @@ -8,18 +8,17 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - "github.com/grafana/grafana/pkg/tsdb/mssql/sqleng" ) const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type msSQLMacroEngine struct { - *sqleng.SQLMacroEngineBase + *SQLMacroEngineBase } -func newMssqlMacroEngine() sqleng.SQLMacroEngine { - return &msSQLMacroEngine{SQLMacroEngineBase: sqleng.NewSQLMacroEngineBase()} +func newMssqlMacroEngine() SQLMacroEngine { + return &msSQLMacroEngine{SQLMacroEngineBase: NewSQLMacroEngineBase()} } func (m *msSQLMacroEngine) Interpolate(query *backend.DataQuery, timeRange backend.TimeRange, @@ -79,7 +78,7 @@ func (m *msSQLMacroEngine) evaluateMacro(timeRange backend.TimeRange, query *bac return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - err := sqleng.SetupFillmode(query, interval, args[2]) + err := SetupFillmode(query, interval, args[2]) if err != nil { return "", err } @@ -114,7 +113,7 @@ func (m *msSQLMacroEngine) evaluateMacro(timeRange backend.TimeRange, query *bac return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - err := sqleng.SetupFillmode(query, interval, args[2]) + err := SetupFillmode(query, interval, args[2]) if err != nil { return "", err } diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/sqleng/macros_test.go similarity index 99% rename from pkg/tsdb/mssql/macros_test.go rename to pkg/tsdb/mssql/sqleng/macros_test.go index b7d1ce9a87d..74c17a3f10d 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/sqleng/macros_test.go @@ -1,4 +1,4 @@ -package mssql +package sqleng import ( "fmt" diff --git a/pkg/tsdb/mssql/proxy.go b/pkg/tsdb/mssql/sqleng/proxy.go similarity index 98% rename from pkg/tsdb/mssql/proxy.go rename to pkg/tsdb/mssql/sqleng/proxy.go index bf91b2f780d..1f25ced6bb0 100644 --- a/pkg/tsdb/mssql/proxy.go +++ b/pkg/tsdb/mssql/sqleng/proxy.go @@ -1,4 +1,4 @@ -package mssql +package sqleng import ( "context" diff --git a/pkg/tsdb/mssql/proxy_test.go b/pkg/tsdb/mssql/sqleng/proxy_test.go similarity index 99% rename from pkg/tsdb/mssql/proxy_test.go rename to pkg/tsdb/mssql/sqleng/proxy_test.go index 9f7ee106747..eb8fbbfb601 100644 --- a/pkg/tsdb/mssql/proxy_test.go +++ b/pkg/tsdb/mssql/sqleng/proxy_test.go @@ -1,4 +1,4 @@ -package mssql +package sqleng import ( "context" diff --git a/pkg/tsdb/mssql/sqleng/sql_engine.go b/pkg/tsdb/mssql/sqleng/sql_engine.go index 350c038b3c0..80e3d29927c 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine.go @@ -14,11 +14,16 @@ import ( "sync" "time" + "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/tsdb/mssql/kerberos" + "github.com/grafana/grafana/pkg/tsdb/mssql/utils" ) // MetaKeyExecutedQueryString is the key where the executed query should get stored @@ -88,6 +93,10 @@ type DataSourceHandler struct { dsInfo DataSourceInfo rowLimit int64 userError string + azureSettings *azsettings.AzureSettings + azureCredentials azcredentials.AzureCredentials + kerberosAuth kerberos.KerberosAuth + driverName string } type QueryJson struct { @@ -113,16 +122,38 @@ func (e *DataSourceHandler) TransformQueryError(logger log.Logger, err error) er return e.queryResultTransformer.TransformQueryError(logger, err) } -func NewQueryDataHandler(userFacingDefaultError string, db *sql.DB, config DataPluginConfiguration, queryResultTransformer SqlQueryResultTransformer, - macroEngine SQLMacroEngine, log log.Logger) (*DataSourceHandler, error) { +func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanceSettings, userFacingDefaultError string, config DataPluginConfiguration, + log log.Logger, azureSettings *azsettings.AzureSettings) (*DataSourceHandler, error) { + queryResultTransformer := utils.MSSQLQueryResultTransformer{ + UserError: userFacingDefaultError, + } + driverName := "mssql" + if config.DSInfo.JsonData.AuthenticationType == azureAuthentication { + driverName = "azuresql" + } + + azureCredentials, err := utils.GetAzureCredentials(settings) + if err != nil { + return nil, fmt.Errorf("error reading azure credentials") + } + + kerberosAuth, err := kerberos.GetKerberosSettings(settings) + if err != nil { + return nil, fmt.Errorf("error getting kerberos settings: %w", err) + } + queryDataHandler := DataSourceHandler{ - queryResultTransformer: queryResultTransformer, - macroEngine: macroEngine, + queryResultTransformer: &queryResultTransformer, + macroEngine: newMssqlMacroEngine(), timeColumnNames: []string{"time"}, log: log, dsInfo: config.DSInfo, rowLimit: config.RowLimit, userError: userFacingDefaultError, + azureSettings: azureSettings, + azureCredentials: azureCredentials, + kerberosAuth: kerberosAuth, + driverName: driverName, } if len(config.TimeColumnNames) > 0 { @@ -133,7 +164,19 @@ func NewQueryDataHandler(userFacingDefaultError string, db *sql.DB, config DataP queryDataHandler.metricColumnTypes = config.MetricColumnTypes } + cnnstr, err := generateConnectionString(config.DSInfo, azureSettings.ManagedIdentityClientId, azureSettings.AzureEntraPasswordCredentialsEnabled, azureCredentials, kerberosAuth, log) + if err != nil { + return nil, err + } + + db, err := newMSSQL(ctx, driverName, config.RowLimit, config.DSInfo, cnnstr, log, settings) + if err != nil { + logger.Error("Failed connecting to MSSQL", "err", err) + return nil, err + } + queryDataHandler.db = db + return &queryDataHandler, nil } diff --git a/pkg/tsdb/mssql/sqleng/sql_engine_test.go b/pkg/tsdb/mssql/sqleng/sql_engine_test.go index 2e3f8da78ee..c4b21b2b849 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine_test.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine_test.go @@ -1,8 +1,10 @@ package sqleng import ( + "encoding/json" "fmt" "net" + "os" "testing" "time" @@ -13,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/mssql/kerberos" "github.com/grafana/grafana/pkg/tsdb/mssql/sqleng/util" ) @@ -436,3 +439,267 @@ func (t *testQueryResultTransformer) TransformQueryError(_ log.Logger, err error func (t *testQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { return nil } + +func genTempCacheFile(t *testing.T, lookups []kerberos.KerberosLookup) string { + content, err := json.Marshal(lookups) + if err != nil { + t.Fatalf("Unable to marshall json for temp lookup: %v", err) + } + + tmpFile, err := os.CreateTemp("", "lookup*.json") + if err != nil { + t.Fatalf("Unable to create temporary file for temp lookup: %v", err) + } + + if _, err := tmpFile.Write(content); err != nil { + t.Fatalf("Unable to write to temporary file for temp lookup: %v", err) + } + + return tmpFile.Name() +} + +func TestGenerateConnectionString(t *testing.T) { + kerberosLookup := []kerberos.KerberosLookup{ + { + Address: "example.host", + DBName: "testDB", + User: "testUser", + CredentialCacheFilename: "/tmp/cache", + }, + } + tmpFile := genTempCacheFile(t, kerberosLookup) + defer func() { + err := os.Remove(tmpFile) + if err != nil { + t.Log(err) + } + }() + + testCases := []struct { + desc string + kerberosCfg kerberos.KerberosAuth + dataSource DataSourceInfo + expConnStr string + }{ + { + desc: "Use Kerberos Credential Cache", + kerberosCfg: kerberos.KerberosAuth{ + CredentialCache: "/tmp/krb5cc_1000", + ConfigFilePath: "/etc/krb5.conf", + UDPConnectionLimit: 1, + }, + dataSource: DataSourceInfo{ + URL: "localhost", + Database: "database", + JsonData: JsonData{ + AuthenticationType: "Windows AD: Credential cache", + }, + }, + expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;krb5-credcachefile=/tmp/krb5cc_1000;", + }, + { + desc: "Use Kerberos Credential Cache File path", + kerberosCfg: kerberos.KerberosAuth{ + CredentialCacheLookupFile: tmpFile, + ConfigFilePath: "/etc/krb5.conf", + UDPConnectionLimit: 1, + }, + dataSource: DataSourceInfo{ + URL: "example.host", + Database: "testDB", + User: "testUser", + JsonData: JsonData{ + AuthenticationType: "Windows AD: Credential cache file", + }, + }, + expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=example.host;database=testDB;krb5-credcachefile=/tmp/cache;", + }, + { + desc: "Use Kerberos Keytab", + kerberosCfg: kerberos.KerberosAuth{ + KeytabFilePath: "/foo/bar.keytab", + ConfigFilePath: "/etc/krb5.conf", + UDPConnectionLimit: 1, + }, + dataSource: DataSourceInfo{ + URL: "localhost", + Database: "database", + User: "foo@test.lab", + JsonData: JsonData{ + AuthenticationType: "Windows AD: Keytab", + }, + }, + expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;krb5-keytabfile=/foo/bar.keytab;", + }, + { + desc: "Use Kerberos Username and Password", + kerberosCfg: kerberos.KerberosAuth{ + ConfigFilePath: "/etc/krb5.conf", + UDPConnectionLimit: 1, + }, + dataSource: DataSourceInfo{ + URL: "localhost", + Database: "database", + User: "foo@test.lab", + DecryptedSecureJSONData: map[string]string{ + "password": "foo", + }, + JsonData: JsonData{ + AuthenticationType: "Windows AD: Username + password", + }, + }, + expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;password=foo;", + }, + { + desc: "Use non-default UDP connection limit", + kerberosCfg: kerberos.KerberosAuth{ + ConfigFilePath: "/etc/krb5.conf", + UDPConnectionLimit: 0, + }, + dataSource: DataSourceInfo{ + URL: "localhost", + Database: "database", + User: "foo@test.lab", + DecryptedSecureJSONData: map[string]string{ + "password": "foo", + }, + JsonData: JsonData{ + AuthenticationType: "Windows AD: Username + password", + }, + }, + expConnStr: "authenticator=krb5;krb5-configfile=/etc/krb5.conf;server=localhost;database=database;user id=foo@test.lab;password=foo;krb5-udppreferencelimit=0;", + }, + + { + desc: "From URL w/ port", + dataSource: DataSourceInfo{ + URL: "localhost:1001", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost;database=database;user id=user;password=;port=1001;", + }, + // When no port is specified, the driver should be allowed to choose + { + desc: "From URL w/o port", + dataSource: DataSourceInfo{ + URL: "localhost", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost;database=database;user id=user;password=;", + }, + // Port 0 should be equivalent to not specifying a port, i.e. let the driver choose + { + desc: "From URL w port 0", + dataSource: DataSourceInfo{ + URL: "localhost:0", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost;database=database;user id=user;password=;", + }, + { + desc: "With instance name", + dataSource: DataSourceInfo{ + URL: "localhost\\instance", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;database=database;user id=user;password=;", + }, + { + desc: "With instance name and port", + dataSource: DataSourceInfo{ + URL: "localhost\\instance:333", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;", + }, + { + desc: "With instance name and ApplicationIntent", + dataSource: DataSourceInfo{ + URL: "localhost\\instance;ApplicationIntent=ReadOnly", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;ApplicationIntent=ReadOnly;database=database;user id=user;password=;", + }, + { + desc: "With ApplicationIntent instance name and port", + dataSource: DataSourceInfo{ + URL: "localhost\\instance:333;ApplicationIntent=ReadOnly", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;ApplicationIntent=ReadOnly;", + }, + { + desc: "With instance name", + dataSource: DataSourceInfo{ + URL: "localhost\\instance", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;database=database;user id=user;password=;", + }, + { + desc: "With instance name and port", + dataSource: DataSourceInfo{ + URL: "localhost\\instance:333", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;", + }, + { + desc: "With instance name and ApplicationIntent", + dataSource: DataSourceInfo{ + URL: "localhost\\instance;ApplicationIntent=ReadOnly", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;ApplicationIntent=ReadOnly;database=database;user id=user;password=;", + }, + { + desc: "With ApplicationIntent instance name and port", + dataSource: DataSourceInfo{ + URL: "localhost\\instance:333;ApplicationIntent=ReadOnly", + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost\\instance;database=database;user id=user;password=;port=333;ApplicationIntent=ReadOnly;", + }, + { + desc: "Defaults", + dataSource: DataSourceInfo{ + Database: "database", + User: "user", + JsonData: JsonData{}, + }, + expConnStr: "server=localhost;database=database;user id=user;password=;", + }, + } + + logger := backend.NewLoggerWith("logger", "mssql.test") + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + connStr, err := generateConnectionString(tc.dataSource, "", false, nil, tc.kerberosCfg, logger) + require.NoError(t, err) + assert.Equal(t, tc.expConnStr, connStr) + }) + } +} diff --git a/pkg/tsdb/mssql/utils/utils.go b/pkg/tsdb/mssql/utils/utils.go index 1e8a2fcb28f..6be125ae6c2 100644 --- a/pkg/tsdb/mssql/utils/utils.go +++ b/pkg/tsdb/mssql/utils/utils.go @@ -3,9 +3,18 @@ package utils import ( "encoding/json" "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "strings" "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" + mssql "github.com/microsoft/go-mssqldb" ) // GetJsonData just gets the json in easier to work with type. It's used on multiple places which isn't super effective @@ -26,3 +35,140 @@ func GetAzureCredentials(settings backend.DataSourceInstanceSettings) (azcredent } return azcredentials.FromDatasourceData(jsonData, settings.DecryptedSecureJSONData) } + +type DebugOnlyLogger interface { + Debug(msg string, args ...interface{}) +} + +// ParseURL tries to parse an MSSQL URL string into a URL object. +func ParseURL(u string, logger DebugOnlyLogger) (*url.URL, error) { + logger.Debug("Parsing MSSQL URL", "url", u) + + // Recognize ODBC connection strings like host\instance:1234 + reODBC := regexp.MustCompile(`^[^\\:]+(?:\\[^:]+)?(?::\d+)?(?:;.+)?$`) + var host string + switch { + case reODBC.MatchString(u): + logger.Debug("Recognized as ODBC URL format", "url", u) + host = u + default: + logger.Debug("Couldn't recognize as valid MSSQL URL", "url", u) + return nil, fmt.Errorf("unrecognized MSSQL URL format: %q", u) + } + return &url.URL{ + Scheme: "sqlserver", + Host: host, + }, nil +} + +type MSSQLQueryResultTransformer struct { + UserError string +} + +func (t *MSSQLQueryResultTransformer) TransformQueryError(logger log.Logger, err error) error { + // go-mssql overrides source error, so we currently match on string + // ref https://github.com/denisenkom/go-mssqldb/blob/045585d74f9069afe2e115b6235eb043c8047043/tds.go#L904 + if strings.HasPrefix(strings.ToLower(err.Error()), "unable to open tcp connection with host") { + logger.Error("Query error", "error", err) + return fmt.Errorf("failed to connect to server - %s", t.UserError) + } + + return err +} + +func (t *MSSQLQueryResultTransformer) GetConverterList() []sqlutil.StringConverter { + return []sqlutil.StringConverter{ + { + Name: "handle MONEY", + InputScanKind: reflect.Slice, + InputTypeName: "MONEY", + ConversionFunc: func(in *string) (*string, error) { return in, nil }, + Replacer: &sqlutil.StringFieldReplacer{ + OutputFieldType: data.FieldTypeNullableFloat64, + ReplaceFunc: func(in *string) (any, error) { + if in == nil { + return nil, nil + } + v, err := strconv.ParseFloat(*in, 64) + if err != nil { + return nil, err + } + return &v, nil + }, + }, + }, + { + Name: "handle SMALLMONEY", + InputScanKind: reflect.Slice, + InputTypeName: "SMALLMONEY", + ConversionFunc: func(in *string) (*string, error) { return in, nil }, + Replacer: &sqlutil.StringFieldReplacer{ + OutputFieldType: data.FieldTypeNullableFloat64, + ReplaceFunc: func(in *string) (any, error) { + if in == nil { + return nil, nil + } + v, err := strconv.ParseFloat(*in, 64) + if err != nil { + return nil, err + } + return &v, nil + }, + }, + }, + { + Name: "handle DECIMAL", + InputScanKind: reflect.Slice, + InputTypeName: "DECIMAL", + ConversionFunc: func(in *string) (*string, error) { return in, nil }, + Replacer: &sqlutil.StringFieldReplacer{ + OutputFieldType: data.FieldTypeNullableFloat64, + ReplaceFunc: func(in *string) (any, error) { + if in == nil { + return nil, nil + } + v, err := strconv.ParseFloat(*in, 64) + if err != nil { + return nil, err + } + return &v, nil + }, + }, + }, + { + Name: "handle UNIQUEIDENTIFIER", + InputScanKind: reflect.Slice, + InputTypeName: "UNIQUEIDENTIFIER", + ConversionFunc: func(in *string) (*string, error) { return in, nil }, + Replacer: &sqlutil.StringFieldReplacer{ + OutputFieldType: data.FieldTypeNullableString, + ReplaceFunc: func(in *string) (any, error) { + if in == nil { + return nil, nil + } + uuid := &mssql.UniqueIdentifier{} + if err := uuid.Scan([]byte(*in)); err != nil { + return nil, err + } + v := uuid.String() + return &v, nil + }, + }, + }, + { + Name: "handle SQL_VARIANT", + InputScanKind: reflect.Pointer, + InputTypeName: "SQL_VARIANT", + ConversionFunc: func(in *string) (*string, error) { return in, nil }, + Replacer: &sqlutil.StringFieldReplacer{ + OutputFieldType: data.FieldTypeNullableString, + ReplaceFunc: func(in *string) (any, error) { + if in == nil { + return nil, nil + } + return in, nil + }, + }, + }, + } +} diff --git a/public/app/api/clients/preferences/v1alpha1/index.ts b/public/app/api/clients/collections/v1alpha1/index.ts similarity index 86% rename from public/app/api/clients/preferences/v1alpha1/index.ts rename to public/app/api/clients/collections/v1alpha1/index.ts index 12d87999e8e..cd2102f6b29 100644 --- a/public/app/api/clients/preferences/v1alpha1/index.ts +++ b/public/app/api/clients/collections/v1alpha1/index.ts @@ -1,9 +1,9 @@ -import { generatedAPI } from '@grafana/api-clients/rtkq/preferences/v1alpha1'; +import { generatedAPI } from '@grafana/api-clients/rtkq/collections/v1alpha1'; import { t } from '@grafana/i18n'; import { notifyApp } from 'app/core/actions'; import { createSuccessNotification, createErrorNotification } from 'app/core/copy/appNotification'; -export const preferencesAPIv1alpha1 = generatedAPI.enhanceEndpoints({ +export const collectionsAPIv1alpha1 = generatedAPI.enhanceEndpoints({ endpoints: { addStar: { onQueryStarted: async (_, { queryFulfilled, dispatch }) => { @@ -39,4 +39,4 @@ export const preferencesAPIv1alpha1 = generatedAPI.enhanceEndpoints({ }); // eslint-disable-next-line no-barrel-files/no-barrel-files -export * from '@grafana/api-clients/rtkq/preferences/v1alpha1'; +export * from '@grafana/api-clients/rtkq/collections/v1alpha1'; diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx index 82636ed6f91..9fed0105c54 100644 --- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx +++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; import { NavModelItem } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { QuickAdd } from './QuickAdd'; @@ -11,6 +11,13 @@ jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), reportInteraction: jest.fn(), + getDataSourceSrv: () => ({ + getList: jest + .fn() + .mockReturnValue([ + { name: 'Test Data Source', uid: 'test-data-source-uid', type: 'grafana-testdata-datasource' }, + ]), + }), }; }); @@ -74,4 +81,31 @@ describe('QuickAdd', () => { from: 'quickadd', }); }); + + describe('Dashboard from template button', () => { + beforeEach(() => { + config.featureToggles.dashboardTemplates = true; + }); + + it('shows a `Dashboard from template` button when the feature flag is enabled', async () => { + setup(); + await userEvent.click(screen.getByRole('button', { name: 'New' })); + expect(screen.getByRole('link', { name: 'Dashboard from template' })).toBeInTheDocument(); + }); + + it('does not show a `Dashboard from template` button when the feature flag is disabled', async () => { + config.featureToggles.dashboardTemplates = false; + setup(); + await userEvent.click(screen.getByRole('button', { name: 'New' })); + expect(screen.queryByRole('link', { name: 'Dashboard from template' })).not.toBeInTheDocument(); + }); + + it('redirects the user to the dashboard from template page when the button is clicked', async () => { + setup(); + + await userEvent.click(screen.getByRole('button', { name: 'New' })); + const link = screen.getByRole('link', { name: 'Dashboard from template' }); + expect(link).toHaveAttribute('href', '/dashboards?templateDashboards=true&source=quickAdd'); + }); + }); }); diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx index f0820da9485..9ea5cba50cf 100644 --- a/public/app/core/components/Login/LoginCtrl.tsx +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo, useState, useCallback } from 'react'; import { t } from '@grafana/i18n'; import { FetchError, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; @@ -54,87 +54,98 @@ interface Props { }) => JSX.Element; } -interface State { - isLoggingIn: boolean; - isChangingPassword: boolean; - showDefaultPasswordWarning: boolean; - loginErrorMessage?: string; -} +export const LoginCtrl = memo(({ resetCode, children }: Props) => { + const [result, setResult] = useState(); + const [isLoggingIn, setIsLoggingIn] = useState(false); + const [isChangingPassword, setIsChangingPassword] = useState(false); + const [showDefaultPasswordWarning, setShowDefaultPasswordWarning] = useState(false); + // oAuth unauthorized sets the redirect error message in the bootdata, hence we need to check the key here + const [loginErrorMessage, setLoginErrorMessage] = useState( + getBootDataErrMessage(config.loginError) + ); -export class LoginCtrl extends PureComponent { - result: LoginDTO | undefined; + const toGrafana = useCallback(() => { + if (config.featureToggles.useSessionStorageForRedirection) { + window.location.assign(config.appSubUrl + '/'); + return; + } - constructor(props: Props) { - super(props); - this.state = { - isLoggingIn: false, - isChangingPassword: false, - showDefaultPasswordWarning: false, - // oAuth unauthorized sets the redirect error message in the bootdata, hence we need to check the key here - loginErrorMessage: getBootDataErrMessage(config.loginError), - }; - } + if (result?.redirectUrl) { + if (config.appSubUrl !== '' && !result.redirectUrl.startsWith(config.appSubUrl)) { + window.location.assign(config.appSubUrl + result.redirectUrl); + } else { + window.location.assign(result.redirectUrl); + } + } else { + window.location.assign(config.appSubUrl + '/'); + } + }, [result]); - changePassword = (password: string) => { - const pw = { - newPassword: password, - confirmNew: password, - oldPassword: 'admin', - }; - - if (this.props.resetCode) { - const resetModel = { - code: this.props.resetCode, + const changePassword = useCallback( + (password: string) => { + const pw = { newPassword: password, - confirmPassword: password, + confirmNew: password, + oldPassword: 'admin', }; + if (resetCode) { + const resetModel = { + code: resetCode, + newPassword: password, + confirmPassword: password, + }; + + getBackendSrv() + .post('/api/user/password/reset', resetModel) + .then(() => { + toGrafana(); + }); + } else { + getBackendSrv() + .put('/api/user/password', pw) + .then(() => { + toGrafana(); + }) + .catch((err) => console.error(err)); + } + }, + [resetCode, toGrafana] + ); + + const changeView = useCallback((showDefaultPasswordWarning: boolean) => { + setIsChangingPassword(true); + setShowDefaultPasswordWarning(showDefaultPasswordWarning); + }, []); + + const login = useCallback( + (formModel: FormModel) => { + setLoginErrorMessage(undefined); + setIsLoggingIn(true); + getBackendSrv() - .post('/api/user/password/reset', resetModel) - .then(() => { - this.toGrafana(); - }); - } else { - getBackendSrv() - .put('/api/user/password', pw) - .then(() => { - this.toGrafana(); + .post('/login', formModel, { showErrorAlert: false }) + .then((result) => { + setResult(result); + if (formModel.password !== 'admin' || config.ldapEnabled || config.authProxyEnabled) { + toGrafana(); + return; + } else { + changeView(formModel.password === 'admin'); + } }) - .catch((err) => console.error(err)); - } - }; - - login = (formModel: FormModel) => { - this.setState({ - loginErrorMessage: undefined, - isLoggingIn: true, - }); - - getBackendSrv() - .post('/login', formModel, { showErrorAlert: false }) - .then((result) => { - this.result = result; - if (formModel.password !== 'admin' || config.ldapEnabled || config.authProxyEnabled) { - this.toGrafana(); - return; - } else { - this.changeView(formModel.password === 'admin'); - } - }) - .catch((err) => { - const fetchErrorMessage = isFetchError(err) ? getErrorMessage(err) : undefined; - this.setState({ - isLoggingIn: false, - loginErrorMessage: fetchErrorMessage || t('login.error.unknown', 'Unknown error occurred'), + .catch((err) => { + const fetchErrorMessage = isFetchError(err) ? getErrorMessage(err) : undefined; + setIsLoggingIn(false); + setLoginErrorMessage(fetchErrorMessage || t('login.error.unknown', 'Unknown error occurred')); }); - }); - }; + }, + [toGrafana, changeView] + ); - passwordlessStart = (formModel: PasswordlessFormModel) => { - this.setState({ - loginErrorMessage: undefined, - isLoggingIn: true, - }); + const passwordlessStart = useCallback((formModel: PasswordlessFormModel) => { + setLoginErrorMessage(undefined); + setIsLoggingIn(true); getBackendSrv() .post('/api/login/passwordless/start', formModel, { showErrorAlert: false }) @@ -144,88 +155,58 @@ export class LoginCtrl extends PureComponent { }) .catch((err) => { const fetchErrorMessage = isFetchError(err) ? getErrorMessage(err) : undefined; - this.setState({ - isLoggingIn: false, - loginErrorMessage: fetchErrorMessage || t('login.error.unknown', 'Unknown error occurred'), - }); + setIsLoggingIn(false); + setLoginErrorMessage(fetchErrorMessage || t('login.error.unknown', 'Unknown error occurred')); }); - }; + }, []); - passwordlessConfirm = (formModel: PasswordlessConfirmationFormModel) => { - this.setState({ - loginErrorMessage: undefined, - isLoggingIn: true, - }); + const passwordlessConfirm = useCallback( + (formModel: PasswordlessConfirmationFormModel) => { + setLoginErrorMessage(undefined); + setIsLoggingIn(true); - getBackendSrv() - .post('/api/login/passwordless/authenticate', formModel, { showErrorAlert: false }) - .then((result) => { - this.result = result; - this.toGrafana(); - return; - }) - .catch((err) => { - const fetchErrorMessage = isFetchError(err) ? getErrorMessage(err) : undefined; - this.setState({ - isLoggingIn: false, - loginErrorMessage: fetchErrorMessage || t('login.error.unknown', 'Unknown error occurred'), + getBackendSrv() + .post('/api/login/passwordless/authenticate', formModel, { showErrorAlert: false }) + .then((result) => { + setResult(result); + toGrafana(); + return; + }) + .catch((err) => { + const fetchErrorMessage = isFetchError(err) ? getErrorMessage(err) : undefined; + setIsLoggingIn(false); + setLoginErrorMessage(fetchErrorMessage || t('login.error.unknown', 'Unknown error occurred')); }); - }); - }; + }, + [toGrafana] + ); - changeView = (showDefaultPasswordWarning: boolean) => { - this.setState({ - isChangingPassword: true, - showDefaultPasswordWarning, - }); - }; + const { loginHint, passwordHint, disableLoginForm, disableUserSignUp } = config; - toGrafana = () => { - if (config.featureToggles.useSessionStorageForRedirection) { - window.location.assign(config.appSubUrl + '/'); - return; - } + return ( + <> + {children({ + isOauthEnabled: isOauthEnabled(), + loginHint, + passwordHint, + disableLoginForm, + disableUserSignUp, + login, + passwordlessStart, + passwordlessConfirm, + showPasswordlessConfirmation: showPasswordlessConfirmation(), + isLoggingIn, + changePassword, + skipPasswordChange: toGrafana, + isChangingPassword, + showDefaultPasswordWarning, + loginErrorMessage, + })} + + ); +}); - if (this.result?.redirectUrl) { - if (config.appSubUrl !== '' && !this.result.redirectUrl.startsWith(config.appSubUrl)) { - window.location.assign(config.appSubUrl + this.result.redirectUrl); - } else { - window.location.assign(this.result.redirectUrl); - } - } else { - window.location.assign(config.appSubUrl + '/'); - } - }; - - render() { - const { children } = this.props; - const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage } = this.state; - const { login, toGrafana, changePassword, passwordlessStart, passwordlessConfirm } = this; - const { loginHint, passwordHint, disableLoginForm, disableUserSignUp } = config; - - return ( - <> - {children({ - isOauthEnabled: isOauthEnabled(), - loginHint, - passwordHint, - disableLoginForm, - disableUserSignUp, - login, - passwordlessStart, - passwordlessConfirm, - showPasswordlessConfirmation: showPasswordlessConfirmation(), - isLoggingIn, - changePassword, - skipPasswordChange: toGrafana, - isChangingPassword, - showDefaultPasswordWarning, - loginErrorMessage, - })} - - ); - } -} +LoginCtrl.displayName = 'LoginCtrl'; export default LoginCtrl; diff --git a/public/app/core/components/NavLandingPage/NavLandingPage.test.tsx b/public/app/core/components/NavLandingPage/NavLandingPage.test.tsx index 0c0ae6987a8..849cfc6d3c8 100644 --- a/public/app/core/components/NavLandingPage/NavLandingPage.test.tsx +++ b/public/app/core/components/NavLandingPage/NavLandingPage.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { TestProvider } from 'test/helpers/TestProvider'; -import { config, setPluginComponentsHook } from '@grafana/runtime'; +import { config, setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; import { createComponentWithMeta } from 'app/features/plugins/extensions/usePluginComponents'; import { NavLandingPage } from './NavLandingPage'; @@ -12,6 +12,10 @@ describe('NavLandingPage', () => { components: [], isLoading: false, })); + setPluginLinksHook(() => ({ + links: [], + isLoading: false, + })); }); const mockSectionTitle = 'Section title'; diff --git a/public/app/core/components/NavLandingPage/NavLandingPage.tsx b/public/app/core/components/NavLandingPage/NavLandingPage.tsx index 4dd7a53d67a..769c37334bc 100644 --- a/public/app/core/components/NavLandingPage/NavLandingPage.tsx +++ b/public/app/core/components/NavLandingPage/NavLandingPage.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import * as React from 'react'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { usePluginComponents } from '@grafana/runtime'; +import { usePluginComponents, usePluginLinks } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; @@ -15,6 +15,7 @@ interface Props { } const EXTENSION_ID = (nodeId: string) => `grafana/dynamic/nav-landing-page/nav-id-${nodeId}/v1`; +const CARDS_EXTENSION_ID = (nodeId: string) => `grafana/dynamic/nav-landing-page/nav-id-${nodeId}/cards/v1`; export function NavLandingPage({ navId, header }: Props) { const { node } = useNavModel(navId); @@ -27,7 +28,23 @@ export function NavLandingPage({ navId, header }: Props) { extensionPointId: EXTENSION_ID(node.id ?? ''), }); - if (isLoading) { + const { links: additionalCards, isLoading: isLoadingCards } = usePluginLinks({ + extensionPointId: CARDS_EXTENSION_ID(node.id ?? ''), + context: { node }, + }); + + // Warn if both extension points are being used (they are mutually exclusive) + React.useEffect(() => { + if (components && components.length > 0 && additionalCards && additionalCards.length > 0) { + console.warn( + `[NavLandingPage] Both NavLandingPage and NavLandingPageCards extensions are registered for "${node.id}". ` + + `The NavLandingPage extension will take precedence and NavLandingPageCards will be ignored. ` + + `Please use only one extension point.` + ); + } + }, [components, additionalCards, node.id]); + + if (isLoading || isLoadingCards) { return null; } @@ -49,6 +66,16 @@ export function NavLandingPage({ navId, header }: Props) { url={child.url ?? ''} /> ))} + {additionalCards?.map((link) => ( + + ))} )}
diff --git a/public/app/core/components/NavLandingPage/NavLandingPageCard.tsx b/public/app/core/components/NavLandingPage/NavLandingPageCard.tsx index 4eb223b4f59..3897066cbd7 100644 --- a/public/app/core/components/NavLandingPage/NavLandingPageCard.tsx +++ b/public/app/core/components/NavLandingPage/NavLandingPageCard.tsx @@ -1,4 +1,5 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; +import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Card, useStyles2 } from '@grafana/ui'; @@ -7,12 +8,24 @@ interface Props { description?: string; text: string; url: string; + category?: string; + onClick?: (event?: React.MouseEvent) => void; } -export function NavLandingPageCard({ description, text, url }: Props) { +const CATEGORY_STYLES = ['primary', 'secondary', 'success', 'warning', 'error'] as const; +type CategoryStyle = (typeof CATEGORY_STYLES)[number]; + +function isCategoryStyle(cat: string): cat is CategoryStyle { + return CATEGORY_STYLES.some((style) => style === cat); +} + +export function NavLandingPageCard({ description, text, url, category, onClick }: Props) { const styles = useStyles2(getStyles); + + const categoryClass = category && isCategoryStyle(category) ? styles[category] : undefined; + return ( - + {text} {description} @@ -31,4 +44,45 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: '-webkit-box', overflow: 'hidden', }), + // Category-based styling + primary: css({ + border: `1px solid ${theme.colors.primary.borderTransparent}`, + backgroundColor: theme.colors.primary.transparent, + '&:hover': { + backgroundColor: theme.colors.primary.transparent, + borderColor: theme.colors.primary.border, + }, + }), + secondary: css({ + border: `1px solid ${theme.colors.secondary.borderTransparent}`, + backgroundColor: theme.colors.secondary.transparent, + '&:hover': { + backgroundColor: theme.colors.secondary.transparent, + borderColor: theme.colors.secondary.border, + }, + }), + success: css({ + border: `1px solid ${theme.colors.success.borderTransparent}`, + backgroundColor: theme.colors.success.transparent, + '&:hover': { + backgroundColor: theme.colors.success.transparent, + borderColor: theme.colors.success.border, + }, + }), + warning: css({ + border: `1px solid ${theme.colors.warning.borderTransparent}`, + backgroundColor: theme.colors.warning.transparent, + '&:hover': { + backgroundColor: theme.colors.warning.transparent, + borderColor: theme.colors.warning.border, + }, + }), + error: css({ + border: `1px solid ${theme.colors.error.borderTransparent}`, + backgroundColor: theme.colors.error.transparent, + '&:hover': { + backgroundColor: theme.colors.error.transparent, + borderColor: theme.colors.error.border, + }, + }), }); diff --git a/public/app/core/components/OptionsUI/NumberInput.tsx b/public/app/core/components/OptionsUI/NumberInput.tsx index b96893096d8..456bf5ced49 100644 --- a/public/app/core/components/OptionsUI/NumberInput.tsx +++ b/public/app/core/components/OptionsUI/NumberInput.tsx @@ -1,5 +1,5 @@ import { debounce } from 'lodash'; -import { PureComponent } from 'react'; +import { memo, useState, useRef, useEffect, useCallback, useMemo } from 'react'; import * as React from 'react'; import { Field, Input } from '@grafana/ui'; @@ -18,11 +18,6 @@ interface Props { suffix?: React.ReactNode; } -interface State { - text: string; - inputCorrected: boolean; -} - /** * This is an Input field that will call `onChange` for blur and enter * @@ -30,101 +25,85 @@ interface State { * by options editor (number and slider), and direclty with in grafana core */ -export class NumberInput extends PureComponent { - state: State = { text: '', inputCorrected: false }; - inputRef = React.createRef(); +export const NumberInput = memo( + ({ id, value, placeholder, autoFocus, onChange, min, max, step, width, fieldDisabled, suffix }: Props) => { + const [text, setText] = useState(''); + const [inputCorrected, setInputCorrected] = useState(false); + const inputRef = useRef(null); - componentDidMount() { - this.setState({ - text: isNaN(this.props.value!) ? '' : `${this.props.value}`, - }); - } + useEffect(() => { + setText(isNaN(value!) ? '' : `${value}`); + }, [value]); - componentDidUpdate(oldProps: Props) { - if (this.props.value !== oldProps.value) { - const text = isNaN(this.props.value!) ? '' : `${this.props.value}`; - if (text !== this.state.text) { - this.setState({ text }); - } - } - } + const updateValue = useCallback(() => { + const txt = inputRef.current?.value; + let corrected = false; + let newValue = ''; + let currentValue = txt !== '' ? Number(txt) : undefined; - updateValue = () => { - const txt = this.inputRef.current?.value; - let corrected = false; - let newValue = ''; - const min = this.props.min; - const max = this.props.max; - let currentValue = txt !== '' ? Number(txt) : undefined; + if (currentValue && !Number.isNaN(currentValue)) { + if (min != null && currentValue < min) { + newValue = min.toString(); + corrected = true; + } else if (max != null && currentValue > max) { + newValue = max.toString(); + corrected = true; + } else { + newValue = txt ?? ''; + } - if (currentValue && !Number.isNaN(currentValue)) { - if (min != null && currentValue < min) { - newValue = min.toString(); - corrected = true; - } else if (max != null && currentValue > max) { - newValue = max.toString(); - corrected = true; - } else { - newValue = txt ?? ''; + setText(newValue); + setInputCorrected(corrected); } - this.setState({ - text: newValue, - inputCorrected: corrected, - }); - } + if (!Number.isNaN(currentValue) && currentValue !== value) { + onChange(currentValue); + } + }, [min, max, value, onChange]); - if (corrected) { - this.updateValueDebounced(); - } + const updateValueDebounced = useMemo(() => debounce(updateValue, 500), [updateValue]); - if (!Number.isNaN(currentValue) && currentValue !== this.props.value) { - this.props.onChange(currentValue); - } - }; - - updateValueDebounced = debounce(this.updateValue, 500); // 1/2 second delay - - onChange = (e: React.FocusEvent) => { - this.setState({ - text: e.currentTarget.value, - }); - this.updateValueDebounced(); - }; - - onKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - this.updateValue(); - } - }; - - renderInput() { - return ( - + const handleChange = useCallback( + (e: React.FocusEvent) => { + setText(e.currentTarget.value); + updateValueDebounced(); + }, + [updateValueDebounced] ); - } - render() { - const { inputCorrected } = this.state; + const handleKeyPress = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + updateValue(); + } + }, + [updateValue] + ); + + const renderInput = () => { + return ( + + ); + }; + if (inputCorrected) { let range = ''; - let { min, max } = this.props; if (max == null) { if (min != null) { range = `< ${min}`; @@ -141,11 +120,13 @@ export class NumberInput extends PureComponent { validationMessageHorizontalOverflow={true} style={{ direction: 'rtl' }} > - {this.renderInput()} + {renderInput()} ); } - return this.renderInput(); + return renderInput(); } -} +); + +NumberInput.displayName = 'NumberInput'; diff --git a/public/app/core/components/OptionsUI/strings.tsx b/public/app/core/components/OptionsUI/strings.tsx index 77294b2314c..e6acc2448aa 100644 --- a/public/app/core/components/OptionsUI/strings.tsx +++ b/public/app/core/components/OptionsUI/strings.tsx @@ -1,96 +1,94 @@ import { css } from '@emotion/css'; +import { memo, useState, useCallback } from 'react'; import * as React from 'react'; import { StandardEditorProps, StringFieldConfigSettings, GrafanaTheme2 } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { stylesFactory, Button, Icon, Input } from '@grafana/ui'; +import { Button, Icon, Input, useStyles2 } from '@grafana/ui'; type Props = StandardEditorProps; -interface State { - showAdd: boolean; -} -export class StringArrayEditor extends React.PureComponent { - state = { - showAdd: false, - }; +export const StringArrayEditor = memo(({ value, onChange, item }: Props) => { + const [showAdd, setShowAdd] = useState(false); + const styles = useStyles2(getStyles); - onRemoveString = (index: number) => { - const { value, onChange } = this.props; - const copy = [...value]; - copy.splice(index, 1); - onChange(copy); - }; + const onRemoveString = useCallback( + (index: number) => { + const copy = [...value]; + copy.splice(index, 1); + onChange(copy); + }, + [value, onChange] + ); - onValueChange = (e: React.KeyboardEvent | React.FocusEvent, idx: number) => { - if ('key' in e) { - if (e.key !== 'Enter') { + const onValueChange = useCallback( + (e: React.KeyboardEvent | React.FocusEvent, idx: number) => { + if ('key' in e) { + if (e.key !== 'Enter') { + return; + } + } + + // Form event, or Enter + const v = e.currentTarget.value.trim(); + if (idx < 0) { + if (v) { + e.currentTarget.value = ''; // reset last value + onChange([...value, v]); + } + setShowAdd(false); return; } - } - const { value, onChange } = this.props; - // Form event, or Enter - const v = e.currentTarget.value.trim(); - if (idx < 0) { - if (v) { - e.currentTarget.value = ''; // reset last value - onChange([...value, v]); + if (!v) { + return onRemoveString(idx); } - this.setState({ showAdd: false }); - return; - } - if (!v) { - return this.onRemoveString(idx); - } + const copy = [...value]; + copy[idx] = v; + onChange(copy); + }, + [value, onChange, onRemoveString] + ); - const copy = [...value]; - copy[idx] = v; - onChange(copy); - }; + const placeholder = item.settings?.placeholder || 'Add text'; - render() { - const { value, item } = this.props; - const { showAdd } = this.state; - const styles = getStyles(config.theme2); - const placeholder = item.settings?.placeholder || 'Add text'; - return ( -
- {value.map((v, index) => { - return ( - this.onValueChange(e, index)} - onKeyDown={(e) => this.onValueChange(e, index)} - suffix={ this.onRemoveString(index)} />} - /> - ); - })} - - {showAdd ? ( + return ( +
+ {value.map((v, index) => { + return ( this.onValueChange(e, -1)} - onKeyDown={(e) => this.onValueChange(e, -1)} - suffix={} + key={`${index}/${v}`} + defaultValue={v || ''} + onBlur={(e) => onValueChange(e, index)} + onKeyDown={(e) => onValueChange(e, index)} + suffix={ onRemoveString(index)} />} /> - ) : ( - - )} -
- ); - } -} + ); + })} -const getStyles = stylesFactory((theme: GrafanaTheme2) => { + {showAdd ? ( + onValueChange(e, -1)} + onKeyDown={(e) => onValueChange(e, -1)} + suffix={} + /> + ) : ( + + )} +
+ ); +}); + +StringArrayEditor.displayName = 'StringArrayEditor'; + +const getStyles = (theme: GrafanaTheme2) => { return { textInput: css({ marginBottom: '5px', @@ -107,4 +105,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { }, }), }; -}); +}; diff --git a/public/app/core/components/Select/TeamPicker.tsx b/public/app/core/components/Select/TeamPicker.tsx index 3f374fc8930..e7b109bae49 100644 --- a/public/app/core/components/Select/TeamPicker.tsx +++ b/public/app/core/components/Select/TeamPicker.tsx @@ -1,6 +1,6 @@ import debounce from 'debounce-promise'; import { isNil } from 'lodash'; -import { Component } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -14,19 +14,11 @@ export interface Props { teamId?: number; } -export interface State { - isLoading: boolean; - value?: SelectableValue; -} +export const TeamPicker = ({ onSelected, className, teamId }: Props) => { + const [isLoading, setIsLoading] = useState(false); + const [value, setValue] = useState | undefined>(); -export class TeamPicker extends Component { - constructor(props: Props) { - super(props); - this.state = { isLoading: false }; - } - - componentDidMount(): void { - const { teamId } = this.props; + useEffect(() => { if (!teamId) { return; } @@ -34,60 +26,58 @@ export class TeamPicker extends Component { getBackendSrv() .get(`/api/teams/${teamId}`) .then((team: Team) => { - this.setState({ - value: { - value: team, - label: team.name, - imgUrl: team.avatarUrl, - }, + setValue({ + value: team, + label: team.name, + imgUrl: team.avatarUrl, }); }); - } + }, [teamId]); - search = debounce( - async (query?: string) => { - this.setState({ isLoading: true }); + const search = useMemo( + () => + debounce( + async (query?: string) => { + setIsLoading(true); - if (isNil(query)) { - query = ''; - } + if (isNil(query)) { + query = ''; + } - return getBackendSrv() - .get(`/api/teams/search?perpage=100&page=1&query=${query}`) - .then((result: { teams: Team[] }) => { - const teams: Array> = result.teams.map((team) => { - return { - value: team, - label: team.name, - imgUrl: team.avatarUrl, - }; - }); + return getBackendSrv() + .get(`/api/teams/search?perpage=100&page=1&query=${query}`) + .then((result: { teams: Team[] }) => { + const teams: Array> = result.teams.map((team) => { + return { + value: team, + label: team.name, + imgUrl: team.avatarUrl, + }; + }); - this.setState({ isLoading: false }); - return teams; - }); - }, - 300, - { leading: true } + setIsLoading(false); + return teams; + }); + }, + 300, + { leading: true } + ), + [] ); - render() { - const { onSelected, className } = this.props; - const { isLoading, value } = this.state; - return ( -
- -
- ); - } -} + return ( +
+ +
+ ); +}; diff --git a/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts b/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts index 03ea4baef00..691c33b4255 100644 --- a/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts +++ b/public/app/core/services/echo/backends/analytics/RudderstackBackend.ts @@ -21,7 +21,21 @@ interface RudderstackAPIOptions { interface Rudderstack { identify: (identifier: string, traits: Properties, options?: RudderstackAPIOptions) => void; - load: (writeKey: string, dataPlaneURL: string, options: { configUrl?: string; destSDKBaseURL?: string }) => void; + // load type set to match Rudderstack v3, for global type compatibility with new version. + load: ( + writeKey: string, + dataPlaneURL: string, + options: { + configUrl?: string; + destSDKBaseURL?: string; + storage?: { + encryption?: { + version: 'V3' | 'legacy'; + }; + migrate?: boolean; + }; + } + ) => void; page: () => void; track: (eventName: string, properties?: Properties) => void; } diff --git a/public/app/core/services/echo/backends/analytics/RudderstackV3Backend.ts b/public/app/core/services/echo/backends/analytics/RudderstackV3Backend.ts new file mode 100644 index 00000000000..dd2c9a1513c --- /dev/null +++ b/public/app/core/services/echo/backends/analytics/RudderstackV3Backend.ts @@ -0,0 +1,166 @@ +import { BuildInfo } from '@grafana/data'; +import { + EchoBackend, + EchoEventType, + isExperimentViewEvent, + isInteractionEvent, + isPageviewEvent, + PageviewEchoEvent, +} from '@grafana/runtime'; + +import { User } from '../../../context_srv'; +import { loadScript } from '../../utils'; + +type Properties = Record; + +interface RudderstackAPIOptions { + Intercom?: { + user_hash: string; + }; +} + +interface Rudderstack { + identify: (identifier: string, traits: Properties, options?: RudderstackAPIOptions) => void; + load: ( + writeKey: string, + dataPlaneURL: string, + options: { + configUrl?: string; + destSDKBaseURL?: string; + storage?: { + encryption?: { + version: 'V3' | 'legacy'; + }; + migrate?: boolean; + }; + } + ) => void; + page: () => void; + track: (eventName: string, properties?: Properties) => void; +} + +declare global { + interface Window { + // We say all methods are undefined because we can't be sure they're there + // and we should be extra cautious + rudderanalytics?: Partial; + } +} + +export interface RudderstackBackendOptions { + writeKey: string; + dataPlaneUrl: string; + buildInfo: BuildInfo; + user?: User; + sdkUrl?: string; + configUrl?: string; + integrationsUrl?: string; +} + +export class RudderstackBackend implements EchoBackend { + supportedEvents = [EchoEventType.Pageview, EchoEventType.Interaction, EchoEventType.ExperimentView]; + + constructor(public options: RudderstackBackendOptions) { + const url = options.sdkUrl || `https://cdn.rudderlabs.com/v3/modern/rsa.min.js`; + loadScript(url); + + // this preloadRudderstack functionality is mirror the loading snippet reference in the Rudderstack + // docs here: https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/installation/#using-cdn + // We don't need a bunch of what's in the snippet, and we can rely on browser support being modern, + // so this is much more terse and only needs to handle the temp setup that pushes calls to the + // rudderstack API into the temp array. + const preloadRudderstack: unknown[] = []; + + const methodNames = [ + 'setDefaultInstanceKey', + 'load', + 'ready', + 'page', + 'track', + 'identify', + 'alias', + 'group', + 'reset', + 'setAnonymousId', + 'startSession', + 'endSession', + 'consent', + 'addCustomIntegration', + ]; + + methodNames.forEach((methodName) => { + // using Object.assign gets around the types being "wrong". + Object.assign(preloadRudderstack, { + [methodName]: function () { + preloadRudderstack.push([methodName].concat(Array.prototype.slice.call(arguments))); + }, + }); + }); + + Object.assign(window, { rudderanalytics: preloadRudderstack }); + + window.rudderanalytics?.load?.(options.writeKey, options.dataPlaneUrl, { + configUrl: options.configUrl, + destSDKBaseURL: options.integrationsUrl, + // these storage settings allow backwards compat across subdomains with the older rudderstack + // version. This would have to be changed to the new setting across all instances at once, + // and wouldn't be able to be rolled back. + storage: { + encryption: { + version: 'legacy', + }, + migrate: false, + }, + }); + + if (options.user) { + const { identifier, intercomIdentifier } = options.user.analytics; + const apiOptions: RudderstackAPIOptions = {}; + + if (intercomIdentifier) { + apiOptions.Intercom = { + user_hash: intercomIdentifier, + }; + } + + window.rudderanalytics?.identify?.( + identifier, + { + email: options.user.email, + orgId: options.user.orgId, + language: options.user.language, + version: options.buildInfo.version, + edition: options.buildInfo.edition, + }, + apiOptions + ); + } + + window.rudderanalytics?.page?.(); + } + + addEvent = (e: PageviewEchoEvent) => { + if (!window.rudderanalytics) { + return; + } + + if (isPageviewEvent(e)) { + window.rudderanalytics.page?.(); + } + + if (isInteractionEvent(e)) { + window.rudderanalytics.track?.(e.payload.interactionName, e.payload.properties); + } + + if (isExperimentViewEvent(e)) { + window.rudderanalytics.track?.('experiment_viewed', { + experiment_id: e.payload.experimentId, + experiment_group: e.payload.experimentGroup, + experiment_variant: e.payload.experimentVariant, + }); + } + }; + + // Not using Echo buffering, addEvent above sends events to GA as soon as they appear + flush = () => {}; +} diff --git a/public/app/core/services/echo/init.ts b/public/app/core/services/echo/init.ts index 3300b4c6bef..d4ff585b700 100644 --- a/public/app/core/services/echo/init.ts +++ b/public/app/core/services/echo/init.ts @@ -146,7 +146,11 @@ async function initRudderstackBackend() { return; } - const { RudderstackBackend } = await import('./backends/analytics/RudderstackBackend'); + const modulePromise = config.featureToggles.rudderstackUpgrade + ? import('./backends/analytics/RudderstackV3Backend') + : import('./backends/analytics/RudderstackBackend'); + + const { RudderstackBackend } = await modulePromise; registerEchoBackend( new RudderstackBackend({ writeKey: config.rudderstackWriteKey, diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index 221c7138f99..56149dbcf55 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -1,20 +1,9 @@ import { css, cx } from '@emotion/css'; -import { memo, PureComponent, ReactElement, useEffect, useRef, useState } from 'react'; +import { memo, ReactElement, useEffect, useRef, useState } from 'react'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { - Button, - ConfirmButton, - Field, - Icon, - Modal, - stylesFactory, - Tooltip, - useStyles2, - Stack, - TextLink, -} from '@grafana/ui'; +import { Button, ConfirmButton, Field, Icon, Modal, Tooltip, useStyles2, Stack, TextLink } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker'; @@ -239,7 +228,7 @@ const OrgRow = memo(({ user, org, isExternalUser, onOrgRemove, onOrgRoleChange } }); OrgRow.displayName = 'OrgRow'; -const getAddToOrgModalStyles = stylesFactory(() => ({ +const getAddToOrgModalStyles = () => ({ modal: css({ width: '500px', }), @@ -249,7 +238,7 @@ const getAddToOrgModalStyles = stylesFactory(() => ({ modalContent: css({ overflow: 'visible', }), -})); +}); interface AddToOrgModalProps { isOpen: boolean; @@ -260,126 +249,104 @@ interface AddToOrgModalProps { onDismiss?(): void; } -interface AddToOrgModalState { - selectedOrg: Organization | null; - role: OrgRole; - roleOptions: Role[]; - pendingOrgId: number | null; - pendingUserId: number | null; - pendingRoles: Role[]; -} +export const AddToOrgModal = memo(({ isOpen, user, userOrgs, onOrgAdd, onDismiss }: AddToOrgModalProps) => { + const [selectedOrg, setSelectedOrg] = useState(null); + const [role, setRole] = useState(OrgRole.Viewer); + const [roleOptions, setRoleOptions] = useState([]); + const [pendingOrgId, setPendingOrgId] = useState(null); + const [pendingUserId, setPendingUserId] = useState(null); + const [pendingRoles, setPendingRoles] = useState([]); + const styles = useStyles2(getAddToOrgModalStyles); -export class AddToOrgModal extends PureComponent { - state: AddToOrgModalState = { - selectedOrg: null, - role: OrgRole.Viewer, - roleOptions: [], - pendingOrgId: null, - pendingUserId: null, - pendingRoles: [], - }; - - onOrgSelect = (org: OrgSelectItem) => { - const userOrg = this.props.userOrgs.find((userOrg) => userOrg.orgId === org.value?.id); - this.setState({ selectedOrg: org.value!, role: userOrg?.role || OrgRole.Viewer }); + const onOrgSelect = (org: OrgSelectItem) => { + const userOrg = userOrgs.find((userOrg) => userOrg.orgId === org.value?.id); + setSelectedOrg(org.value!); + setRole(userOrg?.role || OrgRole.Viewer); if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { fetchRoleOptions(org.value?.id) - .then((roles) => this.setState({ roleOptions: roles })) + .then((roles) => setRoleOptions(roles)) .catch((e) => console.error(e)); } } }; - onOrgRoleChange = (newRole: OrgRole) => { - this.setState({ - role: newRole, - }); + const onOrgRoleChange = (newRole: OrgRole) => { + setRole(newRole); }; - onAddUserToOrg = async () => { - const { selectedOrg, role } = this.state; - this.props.onOrgAdd(selectedOrg!.id, role); + const onAddUserToOrg = async () => { + onOrgAdd(selectedOrg!.id, role); // add the stored userRoles also if (contextSrv.licensedAccessControlEnabled()) { if (contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd)) { - if (this.state.pendingUserId) { - await updateUserRoles(this.state.pendingRoles, this.state.pendingUserId!, this.state.pendingOrgId!); + if (pendingUserId) { + await updateUserRoles(pendingRoles, pendingUserId, pendingOrgId!); // clear pending state - this.setState({ - pendingOrgId: null, - pendingRoles: [], - pendingUserId: null, - }); + setPendingOrgId(null); + setPendingRoles([]); + setPendingUserId(null); } } } }; - onCancel = () => { + const onCancel = () => { // clear selectedOrg when modal is canceled - this.setState({ - selectedOrg: null, - pendingRoles: [], - pendingOrgId: null, - pendingUserId: null, - }); - if (this.props.onDismiss) { - this.props.onDismiss(); + setSelectedOrg(null); + setPendingRoles([]); + setPendingOrgId(null); + setPendingUserId(null); + if (onDismiss) { + onDismiss(); } }; - onRoleUpdate = async (roles: Role[], userId: number, orgId: number | undefined) => { + const onRoleUpdate = async (roles: Role[], userId: number, orgId: number | undefined) => { // keep the new role assignments for user - this.setState({ - pendingRoles: roles, - pendingOrgId: orgId!, - pendingUserId: userId, - }); + setPendingRoles(roles); + setPendingOrgId(orgId!); + setPendingUserId(userId); }; - render() { - const { isOpen, user, userOrgs } = this.props; - const { role, roleOptions, selectedOrg } = this.state; - const styles = getAddToOrgModalStyles(); - return ( - - - - - - - - - - - - - - - ); - } -} + return ( + + + + + + + + + + + + + + + ); +}); +AddToOrgModal.displayName = 'AddToOrgModal'; interface ChangeOrgButtonProps { lockMessage?: string; diff --git a/public/app/features/admin/UserProfile.tsx b/public/app/features/admin/UserProfile.tsx index c2c4d98b0b9..414b8e56000 100644 --- a/public/app/features/admin/UserProfile.tsx +++ b/public/app/features/admin/UserProfile.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { PureComponent, useRef, useState } from 'react'; +import { memo, useRef, useState, useCallback, useEffect } from 'react'; import * as React from 'react'; import { Trans, t } from '@grafana/i18n'; @@ -185,78 +185,68 @@ interface UserProfileRowProps { onChange?: (value: string) => void; } -interface UserProfileRowState { - value: string; - editing: boolean; -} +export const UserProfileRow = memo( + ({ + label, + value: valueProp = '', + locked = false, + lockMessage = '', + inputType = 'text', + onChange, + }: UserProfileRowProps) => { + const [editing, setEditing] = useState(false); + const [value, setValue] = useState(valueProp); + const inputElemRef = useRef(null); -export class UserProfileRow extends PureComponent { - inputElem?: HTMLInputElement; + useEffect(() => { + setValue(valueProp); + }, [valueProp]); - static defaultProps: Partial = { - value: '', - locked: false, - lockMessage: '', - inputType: 'text', - }; + const focusInput = useCallback(() => { + if (inputElemRef.current) { + inputElemRef.current.focus(); + } + }, []); - state = { - editing: false, - value: this.props.value || '', - }; + const onEditClick = useCallback(() => { + if (inputType === 'password') { + // Reset value for password field + setValue(''); + setEditing(true); + setTimeout(focusInput, 0); + } else { + setEditing(true); + setTimeout(focusInput, 0); + } + }, [inputType, focusInput]); - setInputElem = (elem: HTMLInputElement) => { - this.inputElem = elem; - }; + const onCancelClick = useCallback(() => { + setEditing(false); + setValue(valueProp); + }, [valueProp]); - onEditClick = () => { - if (this.props.inputType === 'password') { - // Reset value for password field - this.setState({ editing: true, value: '' }, this.focusInput); - } else { - this.setState({ editing: true }, this.focusInput); - } - }; + const onInputChange = useCallback((event: React.ChangeEvent, status?: LegacyInputStatus) => { + if (status === LegacyInputStatus.Invalid) { + return; + } - onCancelClick = () => { - this.setState({ editing: false, value: this.props.value || '' }); - }; + setValue(event.target.value); + }, []); - onInputChange = (event: React.ChangeEvent, status?: LegacyInputStatus) => { - if (status === LegacyInputStatus.Invalid) { - return; - } + const onInputBlur = useCallback((event: React.FocusEvent, status?: LegacyInputStatus) => { + if (status === LegacyInputStatus.Invalid) { + return; + } - this.setState({ - value: event.target.value, - }); - }; + setValue(event.target.value); + }, []); - onInputBlur = (event: React.FocusEvent, status?: LegacyInputStatus) => { - if (status === LegacyInputStatus.Invalid) { - return; - } + const onSave = useCallback(() => { + if (onChange) { + onChange(value); + } + }, [onChange, value]); - this.setState({ - value: event.target.value, - }); - }; - - focusInput = () => { - if (this.inputElem && this.inputElem.focus) { - this.inputElem.focus(); - } - }; - - onSave = () => { - if (this.props.onChange) { - this.props.onChange(this.state.value); - } - }; - - render() { - const { label, locked, lockMessage, inputType } = this.props; - const { value } = this.state; const labelClass = cx( 'width-16', css({ @@ -275,26 +265,26 @@ export class UserProfileRow extends PureComponent{label} - {this.state.editing ? ( + {editing ? ( ) : ( - {this.props.value} + {valueProp} )} {t('admin.user-profile.edit-button', 'Edit')} @@ -302,7 +292,9 @@ export class UserProfileRow extends PureComponent ); } -} +); + +UserProfileRow.displayName = 'UserProfileRow'; interface LockedRowProps { label: string; diff --git a/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap index 4c1b0bb8b65..e36613807ec 100644 --- a/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/PanelAlertTabContent.test.tsx.snap @@ -52,9 +52,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -90,9 +88,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap index fdd49bc80d7..7aa2e489209 100644 --- a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap +++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap @@ -78,9 +78,7 @@ exports[`RuleEditor grafana recording rules can create new grafana recording rul "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -208,9 +206,7 @@ exports[`RuleEditor grafana recording rules can create new grafana recording rul "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap index 9ffe68ad3b4..1ff6c2a0502 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap @@ -79,9 +79,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -117,9 +115,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -249,9 +245,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -287,9 +281,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -422,9 +414,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -460,9 +450,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -598,9 +586,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -636,9 +622,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], @@ -771,9 +755,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -809,9 +791,7 @@ exports[`Can create a new grafana managed alert using simplified routing switch "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts b/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts index 7cfe57b95f6..df6a317d071 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts +++ b/public/app/features/alerting/unified/components/rules/Filter/useRuleFilterAutocomplete.ts @@ -15,10 +15,19 @@ function getExternalRuleDataSources() { return getRulesDataSources().filter((ds: DataSourceInstanceSettings) => !!ds?.url); } +const THRESHOLD_LIMIT = 500; + +function createInfoOption(message: string): ComboboxOption { + return { + label: message, + value: '__GRAFANA_INFO_OPTION__', + infoOption: true, + }; +} + export function useNamespaceAndGroupOptions(): { namespaceOptions: (inputValue: string) => Promise>>; - allGroupNames: string[]; - isLoadingNamespaces: boolean; + groupOptions: (inputValue: string) => Promise>>; namespacePlaceholder: string; groupPlaceholder: string; } { @@ -50,17 +59,11 @@ export function useNamespaceAndGroupOptions(): { const namespaceOptions = useCallback( async (inputValue: string) => { - // Grafana namespaces - const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: 1000 }).unwrap(); - const grafanaFolders: Array> = Array.from( + // Grafana namespaces - fetch with limit to check threshold + const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: THRESHOLD_LIMIT + 1 }).unwrap(); + const grafanaFolderNames = Array.from( new Set(grafanaResponse.data.groups.map((g: GrafanaPromRuleGroupDTO) => g.file || 'default')) - ) - .map((name) => ({ - label: name, - value: name, - description: t('alerting.rules-filter.grafana-folder', 'Grafana folder'), - })) - .sort((a, b) => collator.compare(a.label ?? '', b.label ?? '')); + ); // External namespaces const namespaceNameSet = new Set(); @@ -68,7 +71,7 @@ export function useNamespaceAndGroupOptions(): { fetchExternalGroups({ ruleSource: { uid: ds.uid }, excludeAlerts: true, - groupLimit: 500, + groupLimit: THRESHOLD_LIMIT + 1, notificationOptions: { showErrorAlert: false }, }).unwrap() ); @@ -78,6 +81,29 @@ export function useNamespaceAndGroupOptions(): { res.value.data.groups.forEach((group: { file?: string }) => namespaceNameSet.add(group.file || 'default')); } } + + const totalNamespaces = grafanaFolderNames.length + namespaceNameSet.size; + + // If we have more than THRESHOLD_LIMIT unique namespaces, show info message + if (totalNamespaces > THRESHOLD_LIMIT) { + return [ + createInfoOption( + t( + 'alerting.rules-filter.namespace-autocomplete-unavailable', + 'Due to large number of folders, autocomplete is not available' + ) + ), + ]; + } + + const grafanaFolders: Array> = grafanaFolderNames + .map((name) => ({ + label: name, + value: name, + description: t('alerting.rules-filter.grafana-folder', 'Grafana folder'), + })) + .sort((a, b) => collator.compare(a.label ?? '', b.label ?? '')); + const externalNamespaces = Array.from(namespaceNameSet) .map(formatNamespaceOption) .sort((a, b) => collator.compare(a.label ?? '', b.label ?? '')); @@ -89,12 +115,37 @@ export function useNamespaceAndGroupOptions(): { [fetchGrafanaGroups, fetchExternalGroups, formatNamespaceOption] ); - const allGroupNames: string[] = []; - const isLoadingNamespaces = false; + const groupOptions = useCallback( + async (inputValue: string) => { + const grafanaResponse = await fetchGrafanaGroups({ limitAlerts: 0, groupLimit: THRESHOLD_LIMIT + 1 }).unwrap(); + const groupNames = Array.from(new Set(grafanaResponse.data.groups.map((g: GrafanaPromRuleGroupDTO) => g.name))); + + // If we have more than THRESHOLD_LIMIT unique groups, show info message + if (groupNames.length > THRESHOLD_LIMIT) { + return [ + createInfoOption( + t( + 'alerting.rules-filter.group-autocomplete-unavailable', + 'Due to large number of groups, autocomplete is not available' + ) + ), + ]; + } + + const options: Array> = groupNames + .map((name) => ({ label: name, value: name })) + .sort((a, b) => collator.compare(a.label ?? '', b.label ?? '')); + + const filtered = filterBySearch(options, inputValue); + return filtered; + }, + [fetchGrafanaGroups] + ); + const namespacePlaceholder = t('alerting.rules-filter.filter-options.placeholder-namespace', 'Select namespace'); const groupPlaceholder = t('grafana.select-group', 'Select group'); - return { namespaceOptions, allGroupNames, isLoadingNamespaces, namespacePlaceholder, groupPlaceholder }; + return { namespaceOptions, groupOptions, namespacePlaceholder, groupPlaceholder }; } export function useLabelOptions(): { diff --git a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap index 298f5e0542a..6dd5b9af077 100644 --- a/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap +++ b/public/app/features/alerting/unified/rule-editor/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap @@ -81,9 +81,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1 "type": "and", }, "query": { - "params": [ - "B", - ], + "params": [], }, "reducer": { "params": [], @@ -119,9 +117,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1 "type": "and", }, "query": { - "params": [ - "C", - ], + "params": [], }, "reducer": { "params": [], diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 8ac1e1a8c58..fb46822f0c3 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -16,11 +16,11 @@ import { GrafanaRuleListItem } from './GrafanaRuleListItem'; import LoadMoreHelper from './LoadMoreHelper'; import { UnknownRuleListItem } from './components/AlertRuleListItem'; import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader'; +import { hasClientSideFilters } from './hooks/grafanaFilter'; import { GrafanaRuleWithOrigin, PromRuleWithOrigin, RuleWithOrigin, - hasClientSideFilters, useFilteredRulesIteratorProvider, } from './hooks/useFilteredRulesIterator'; import { FRONTEND_LIST_PAGE_SIZE, getSearchApiGroupPageSize } from './paginationLimits'; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx index 2e2c2fda607..546d500ace2 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedDataSourceLoader.tsx @@ -14,7 +14,7 @@ import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; import { LoadMoreButton } from './components/LoadMoreButton'; import { NoRulesFound } from './components/NoRulesFound'; -import { getDatasourceFilter } from './hooks/filters'; +import { getDatasourceFilter } from './hooks/datasourceFilter'; import { toIndividualRuleGroups, usePrometheusGroupsGenerator } from './hooks/prometheusGroupsGenerator'; import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits'; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index d1835d29757..a39509608b3 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -18,7 +18,7 @@ import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; import { LoadMoreButton } from './components/LoadMoreButton'; import { NoRulesFound } from './components/NoRulesFound'; -import { getGrafanaFilter } from './hooks/filters'; +import { getGrafanaFilter } from './hooks/grafanaFilter'; import { toIndividualRuleGroups, useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator'; import { useLazyLoadPrometheusGroups } from './hooks/useLazyLoadPrometheusGroups'; import { FRONTED_GROUPED_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits'; diff --git a/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx index dde3e983ad9..1cc702038ac 100644 --- a/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/filter/RulesFilter.v2.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Controller, FormProvider, SubmitHandler, useForm, useFormContext } from 'react-hook-form'; import { ContactPointSelector } from '@grafana/alerting/unstable'; @@ -55,6 +55,25 @@ const canRenderContactPointSelector = contextSrv.hasPermission(AccessControlActi const radioGroupCompactClass = css({ width: 'max-content' }); +// Helper to create a wrapped options function that captures typed input when threshold is exceeded +function createThresholdAwareOptions( + optionsFunc: (inputValue: string) => Promise, + setValue: (value: string) => void, + fieldName: string +) { + return async (inputValue: string): Promise => { + const options = await optionsFunc(inputValue); + const exceeded = options.length === 1 && options[0].infoOption === true; + + // If threshold exceeded and user is typing, capture the typed value + if (exceeded && inputValue) { + setValue(inputValue); + } + + return options; + }; +} + type SearchQueryForm = { query: string; }; @@ -85,11 +104,9 @@ export default function RulesFilter({ viewMode, onViewModeChange }: RulesFilterP }; const handleAdvancedFilters: SubmitHandler = (values) => { - const newFilter = formAdvancedFiltersToRuleFilter(values); - updateFilters(newFilter); - + updateFilters(formAdvancedFiltersToRuleFilter(values)); trackFilterButtonApplyClick(values, pluginsFilterEnabled); - setIsPopupOpen(false); // Should close popup after applying filters? + setIsPopupOpen(false); }; const handleClearFilters = () => { @@ -248,8 +265,7 @@ const FilterOptions = ({ onSubmit, onClear, pluginsFilterEnabled }: FilterOption const defaultValues = searchQueryToDefaultValues(filterState); // Fetch namespace and group data from all sources (optimized for filter UI) - const { namespaceOptions, allGroupNames, isLoadingNamespaces, namespacePlaceholder, groupPlaceholder } = - useNamespaceAndGroupOptions(); + const { namespaceOptions, groupOptions, namespacePlaceholder, groupPlaceholder } = useNamespaceAndGroupOptions(); const { labelOptions } = useLabelOptions(); @@ -294,13 +310,11 @@ const FilterOptions = ({ onSubmit, onClear, pluginsFilterEnabled }: FilterOption @@ -373,15 +387,22 @@ function LabelsField({ function NamespaceField({ namespaceOptions, namespacePlaceholder, - isLoadingNamespaces, portalContainer, }: { - namespaceOptions: (inputValue: string) => Promise>; + namespaceOptions: ( + inputValue: string + ) => Promise>; namespacePlaceholder: string; - isLoadingNamespaces: boolean; portalContainer?: HTMLElement; }) { - const { control } = useFormContext(); + const { control, setValue } = useFormContext(); + + const wrappedOptions = useCallback( + (inputValue: string) => + createThresholdAwareOptions(namespaceOptions, (value) => setValue('namespace', value), 'namespace')(inputValue), + [namespaceOptions, setValue] + ); + return ( <>