Compare commits

...

146 Commits

Author SHA1 Message Date
alexandra vargas
afdbc63250 Add dashboard extraction test 2026-01-14 12:09:58 +01:00
alexandra vargas
f67bd022be refactor tests to be consistent with the rest, use require 2026-01-14 10:33:55 +01:00
alexandra vargas
ad989ae200 Improve dashboard detection and add unit test 2026-01-14 10:17:47 +01:00
alexandra vargas
fe6c2cdfee Create unit tests for parser.go 2026-01-13 17:04:28 +01:00
alexandra vargas
84b081ce37 Merge branch 'axelav/dash-validator-app-mvp' into axelav/dash-validator-app-prometheus-poc 2026-01-13 15:20:06 +01:00
alexandra vargas
d4ae044801 Merge remote-tracking branch 'origin' into axelav/dash-validator-app-mvp 2026-01-13 14:58:15 +01:00
Misi
c9a14f1774 IAM: Target resource authorization for TeamBinding (#116117)
* wip

* Review VerbGet vs VerbGetPermissions

* Fix tests
2026-01-13 14:45:18 +01:00
Will Browne
d2b788eb53 Plugins: Remove angular details from meta API (#116194)
remove angular details from meta API
2026-01-13 13:41:40 +00:00
Ashley Harrison
dffae66fdc Storybook: Add workflow to deploy canary storybook (#116138)
* add first attempt at storybook deploy action for canary

* don't run on push to main yet!

* add CODEOWNER
2026-01-13 13:10:04 +00:00
alerting-team[bot]
5dbbe8164b Alerting: Update alerting module to 98a49ed9557fd9b5f33ecb77cbaa0748f13dc568 (#116197)
* [create-pull-request] automated change

* update prometheus-alertmanager

---------

Co-authored-by: titolins <8942194+titolins@users.noreply.github.com>
Co-authored-by: Tito Lins <tito.linsesilva@grafana.com>
2026-01-13 12:27:35 +00:00
Tobias Skarhed
d1064da4cd Scopes: Add RTK Query API client for caching (#115494)
* Scopes API client

* Initial RTK query commit

* Copy API client from generated enterprise folder

* Mock ScopesApiClient for integration tests

* Update e2e tests

* Handle group expansion for dashboard navigation

* Extract integration test mocks

* Move mock to only be for integration tests

* Update path for enterprise sync script

* Re-export mockData

* Disregard caching for search

* Leave name parameters empty

* Disable subscriptions for client requests

* Add functionality to reset cache between mocked requests

* Use grafana-test-utils for scopes integration tests

* Rollback mock setup

* Remove store form window object

* Remove cache helper

* Restore scopenode search functionality

* Improve request erro handling

* Clean up subscription in case subscription: false lies

* Fix logging security risk

* Rewrite tests to cover RTK query usage and improve error catching

* Update USE_LIVE_DATA to be consistent

* Remove unused timout parameter

* Fix error handling

* Make dashboard-navigation test pass
2026-01-13 13:09:08 +01:00
Tito Lins
b57b8d4359 fix: handle go mod issues (#116187) 2026-01-13 12:48:16 +01:00
Mustafa Sencer Özcan
5219ccddb6 fix: improve resilience for unified storage and search service grpc clients (#116122)
* fix: reliability

* fix: resilience

* fix: add connection backoff

* fix: reduce backoff
2026-01-13 11:42:21 +00:00
Ashley Harrison
c95e3da2d5 Theme: Convert themes to json and define schemas using zod (#116006)
* convert all theme files to json

* automatically discover extra themes in go backend

* use zod

* error tidy up

* error tidy up p2

* generate theme json schema from zod

* generate theme list at build time, don't do it at runtime

* make name and id required in the theme schema
2026-01-13 11:13:11 +00:00
Gareth
43d9fbc056 Tempo: Fix search streaming queries (#116136)
* Tempo: Fix search queries

* apply variables for metrics streaming queries
2026-01-13 19:47:44 +09:00
Konrad Lalik
7b80c44ac7 Alerting: Fix label value search not filtering results (#116133)
Fixes the issue where typing in the label value dropdown would display
all values instead of filtering them based on the search input.

The bug was in `createAsyncValuesLoader` which was ignoring the
`valueQuery` parameter and returning all combined values instead of
the filtered subset.

Changes:
- Rename `_inputValue` parameter to `valueQuery` to indicate it should be used
- Filter combined values based on case-insensitive search query
- Return only filtered values instead of all values

Tests:
- Add test to verify correct values are shown for each label key
- Add test to verify search filtering works correctly
- Improve test infrastructure with proper portal container and element mocking
  for virtualized dropdown rendering
2026-01-13 11:43:07 +01:00
alexandra vargas
7038ced64e Merge branch 'axelav/dash-validator-app-mvp' into axelav/dash-validator-app-prometheus-poc 2026-01-13 11:38:03 +01:00
Rafael Bortolon Paulovic
98f271f345 chore(unified): remove unifiedStorageSearchSprinkles feature toggle (#116139)
chore: remove unifiedStorageSearchSprinkles feature flag

The feature flag is no longer needed because:
- OSS: usageinsights code doesn't exist in OSS builds
- Enterprise On-Prem: uses local SQL storage when enable_search=true
- Cloud: explicitly configures sprinkles_api_server URL

The sprinkles functionality now works automatically based on:
- enable_search config (enforced true for unified storage mode 5)
- sprinkles_api_server config (empty = local storage, set = remote API)
2026-01-13 11:24:13 +01:00
Vardan Torosyan
60c4fab063 [Docs] Add Synthentic Monitoring app to the list of RBAC supported apps (#116167)
* [Docs] Add Synthentic Monitoring app to the list of RBAC supported apps

* Run prettier
2026-01-13 11:23:33 +01:00
alexandra vargas
f117691340 Merge remote-tracking branch 'origin' into axelav/dash-validator-app-mvp 2026-01-13 11:13:53 +01:00
alexandra vargas
c99eb8c62e Add dashvalidator app to Dockerfile for Go workspace validation 2026-01-13 11:13:32 +01:00
Ihor Yeromin
ce8663ac24 SQL Expressions: Filter Dashboard datasource queries from schema fetching (#116129)
* fix(sql expression): sql schema frontend datasources filtering

* add one more test
2026-01-13 10:26:33 +01:00
Yulia Shanyrova
5dd9a14903 Plugins: Fix the flaky configuration tab on the plugin details page for cloud instances (#114922)
Fix flaky configuration tab for plugin details page at cloud instances
2026-01-13 09:55:52 +01:00
Roberto Jiménez Sánchez
68bf19d840 Provisioning: handle resource version conflicts in connection CRUDL test (#116184)
fix: handle resource version conflicts in connection CRUDL test

After updating a connection resource, the controller may update the
resource status, changing the resource version. This causes the delete
operation to fail with a resource version conflict.

Add retry logic to handle conflicts gracefully by retrying the delete
operation when encountering resource version conflicts.
2026-01-13 08:53:54 +00:00
Costa Alexoglou
220c29de89 fix: 401 in grafana live spam (#116140) 2026-01-13 09:46:06 +01:00
Oscar Kilhed
91ab753368 Dynamic Dashboards: Fix navigation to repeated panels and update outline when lazy items repeat (#116030)
Dashboard Outline: Fix navigation to repeated panels and lazy-loaded repeats

- Remove cursor: not-allowed styling from repeated panels in outline
- Add RepeatsUpdatedEvent to notify when panel repeats are populated
- Subscribe to RepeatsUpdatedEvent in DashboardEditPane to refresh outline
- Remove memoization from visibleChildren to ensure outline updates on re-render
2026-01-13 08:43:50 +01:00
Alex Khomenko
250ca7985f Provisioning: Add Connections page (#116060)
* Provisioning: Add connections page

* Provisioning: Add connections form

* Provisioning: Add connections form

* Update fields

* Fix generated name

* Update connection name

* Add edit page

* error handling

* Form validation

* Add Connections button

* Cleanup

* Extract ConnectionFormData type

* Add list test and separate empty states

* Add form test

* Update tests

* i18n

* Cleanup

* Use SecretTextArea from grafana-ui

* Fix breadcrumbs

* tweaks

* Add missing URL

* Switch to ShowConfirmModalEvent

* i18n

* redirect to list on success

* add timeout

* Fix tags invalidation
2026-01-13 08:25:40 +02:00
Hugo Häggmark
b57ed32484 chore: remove app/core/config barrel files (#116068) 2026-01-13 06:23:21 +01:00
Galen Kistler
d0217588a3 LogsDrilldown: Remove exploreLogsLimitedTimeRange flag (#116177)
chore: remove flag
2026-01-12 22:43:01 +00:00
Denis Vodopianov
ce9ab6a89a Add non-boolean feature flags support to the StaticProvider (#115085)
* initial commit

* add support of integerts

* finialise the static provider

* minor refactoring

* the rest

* revert:  the rest

* add new thiongs

* more tests added

* add ff parsing tests to check if types are handled correctly

* update tests according to recent changes

* address golint issues

* Update pkg/setting/setting_feature_toggles.go

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* fix rebase issues

* addressing review comments

* add test cases for enterprise

* handle enterprise cases

* minor refactoring to make api a bit easier to debug

* make test names a bit more precise

* fix linter

* add openfeature sdk to goleak ignore in testutil

* Remove only boolean check in ff gen tests

* add non-boolean types top the doc in default.ini and doc string in FeatureFlag type

* apply remarks, add docs to sample.ini

* reflect changes in feature flags in the public grafana configuration doc

* fix doc formatting

* apply suggestions to the doc file

---------

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
2026-01-12 22:53:23 +01:00
Will Assis
8c8efd2494 unified-storage: skip sqlkv/sqlbackend compatibility tests in sqlite (#116164) 2026-01-12 16:31:29 -05:00
Will Assis
69ccfd6bfc unified-storage: fix sharedwithme search not returning folders (#116089)
* unified-storage: fix dashboard sharedwithme search not returning folders shared with the user
2026-01-12 15:33:34 -05:00
Nick Richmond
53aa5e8f7f MetricsDrilldown: Remove exploreMetricsRelatedLogs feature toggle (#116090)
chore: remove unused exploreMetricsRelatedLogs feature toggle
2026-01-12 12:52:40 -05:00
Ida Štambuk
69bf3068b3 Dashboards: Never show scopes variables (#116132) 2026-01-12 18:52:23 +01:00
Will Assis
1263a3d364 unified-storage: HappyPath and notifier tests + couple of bugfixes (#116087)
* unified-storage: couple of bugfixes and enable HappyPath and notifier sqlkv tests
2026-01-12 12:17:41 -05:00
Daniele Stefano Ferru
e4b79e2fc8 Provisioning: Add Validation and Mutation for Connection resource (#115596)
* WIP: mutator added, start working on validator

* first validator iteration

* second validator iteration

* wip: working on integration tests

* re-working mutation and validation, using Connection interface

* fixing some rebase things

* fixing integration tests

* formatting

* fixing unit tests

* k8s codegen

* linting

* moving tests which are available only for enterprise

* addressing comments: using repo config for connections, updating tests

* addressing comments: adding some more info in the app and installation

* fixing app data

* addressing comments: updating connection implementation

* addressing comments

* formatting

* fixing tests
2026-01-12 17:52:00 +01:00
Haris Rozajac
0d1ec94548 Dashboard Schema V2: Activate dashboard ds queries (#116085)
* support dashboard ds

* add test for getPanelDataSource
2026-01-12 08:15:10 -07:00
Jack Westbrook
23a51ec9c5 CI: Fix frontend package validation (#116104)
* ci(frontend-lint): add frontend package change detection and add validate packed packages lint step

* ci(change-detection): add validate-npm-packages.sh to frontend-packages list

* ci(gh-workflows): add actions globs to frontend-packages detection

* ci(gh-workflows): fix typo - > _

* ci(frontend-lint): add missing needs

* chore(i18n): fix publint erroring for custom condition pointing to .cjs file

* ci(validate-npm-packages): make profile node16 default

* chore(validate-npm-packages): remove shellcheck disable comment
2026-01-12 16:08:32 +01:00
Dafydd
51dcdd3499 Datasources: Experimental API group names use full plugin IDs (#112961) 2026-01-12 15:01:40 +00:00
Larissa Wandzura
880bc23c85 Docs: Added a troubleshooting guide for MSSQL, plus some updates (#116088)
* added troubleshooting guide

* cleaned up the intro doc

* cleaned up the before you begin section in the configure doc

* changed a note to a tip

* changed to troubleshooting for cohesion

* final edits

* minor clean up item

* added note about Kerberos not being supported in Cloud

* punctuation fixes
2026-01-12 14:57:45 +00:00
Yunwen Zheng
6dc604c2ea RecentlyViewedDashboards: Add instrumentations (#116036)
RecentlyViewedDashboards: Add instrumentation
2026-01-12 09:56:15 -05:00
Matias Chomicki
77c500dc01 logsExploreTableDefaultVisualization: remove feature flag (#116127) 2026-01-12 14:46:47 +00:00
Kristina Demeshchik
bec4d225b3 FieldConfig: Fix multiple value mappings in field overrides being overwritten (#116027)
* multiple value mappingg overrides

* add comment for clarity

* remove extra check
2026-01-12 09:29:16 -05:00
Ivana Huckova
b91ca14f48 Icons: Add brain icon (#116023)
* Icons: Add brain icon

* lint

* Add brain to cached icons
2026-01-12 13:38:18 +01:00
Matias Chomicki
2aedbdb76f processing: support duplicated keys when parsing json logs (#116116)
* processing: support duplicated keys when parsing json logs

* Add regression test

* prettier
2026-01-12 13:37:17 +01:00
Rafael Bortolon Paulovic
0d7f46c08a chore(unified): remove unifiedStorageSearch feature toggle (#116109) 2026-01-12 13:22:48 +01:00
Ryan McKinley
1b52718c23 Search: include panel titles and types in index (#115742) 2026-01-12 13:21:03 +01:00
Naimesh Patel
e61e406440 Explore: Add keyboard shortcut to run queries (#111675) (#115811)
* Explore: add keyboard shortcut to run queries (#111675)

* Update mock

* Fix linting

---------

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
2026-01-12 13:19:47 +01:00
Jack Westbrook
5cb4c311dc Chore: Eslint ignore webpack.config barrel files (#116115)
chore(eslint): ignore decoupled plugins webpack configs barrel files
2026-01-12 11:13:11 +00:00
Matheus Macabu
586410d8b5 Build: Fix running e2e tests for Cypress with Dagger (#116105) 2026-01-12 11:12:40 +01:00
james-rms
a0e894c6d8 Documentation: Fix typo in plugin-sign.md heading (#115812) 2026-01-12 09:57:06 +00:00
Roberto Jiménez Sánchez
e4796b1de3 Provisioning: Add fieldSelector for Repository by spec.connection.name (#116063)
* Provisioning: Add fieldSelector for Repository by spec.connection.name

This change adds the ability to filter repositories by their connection
name using Kubernetes field selectors, enabling queries like:

  kubectl get repositories --field-selector spec.connection.name=my-connection

Implementation:
- Add RepositoryGetAttrs and RepositoryToSelectableFields functions
- Register field label conversion for spec.connection.name in InstallSchema
- Extend generic storage to support custom selectable fields via
  NewRegistryStoreWithSelectableFields
- Add unit tests for repository field functions
- Add integration tests for field selector functionality

* Simplify predicateFunc handling with custom attrFunc

Remove unnecessary custom predicateFunc wrapper when using a custom
GetAttrs function. When attrFunc is provided via StoreOptions, passing
nil for predicateFunc allows the default behavior to create the
appropriate SelectionPredicate automatically.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 09:31:25 +00:00
Miklós Tolnai
86a3aae204 InteractiveTable: Extend sort options with disableSortRemove and sortDescFirst (#115352)
* add disableSortRemove option

* add sortDescFirst to Column

* pass sortDescFirst only if it is set
2026-01-12 10:30:38 +01:00
Jack Westbrook
e0ad4eb7ed Chore: Remove core actions barrel file (#98149)
* refactor(frontend): update core/actions imports to avoid barrel file

* chore(frontend): delete app/core/actions barrel file

* refactor(frontend): replace more barrel file imports

* refactor(frontend): replace more core/actions imports

* rerun ci
2026-01-12 10:17:02 +01:00
Gonzalo Trigueros Manzanas
f0c95a0a10 Provisioning: Add new error framework to handle folder creation failures gracefully. (#114824)
* Implement hierarchical error handling for folder creation failures

This commit implements hierarchical error handling to improve sync robustness
when folder creation fails. Instead of failing the entire sync, the system now:

1. Tracks failed folder creations and automatically skips nested resources
2. Records skipped resources with FileActionIgnored (doesn't count toward error limits)
3. Allows other folder hierarchies to continue processing
4. Prevents folder deletion when child resource deletions fail

Key Changes:

- Add PathCreationError type to track which folder path failed
- Modify progress recorder to automatically detect and track failures via Record()
- Add IsNestedUnderFailedCreation() and HasFailedDeletionsUnder() checks
- Update full and incremental sync to skip nested resources after folder failures
- Deletions proceed even if parent folder creation failed (resource may exist from previous sync)
- FileActionIgnored results don't count toward error limits

Example behavior improvement:
Before: /monitoring folder creation fails → all nested resources fail → other folders never processed
After: /monitoring folder creation fails → nested resources ignored → /applications folder succeeds

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* provisioning: refactor hierarchical errors in folder management.

* Move test to the corresponding package

* Refactor timeout handling in applyChanges functions

- Introduced wrapWithTimeout function to streamline timeout context management for applyChange calls.
- Updated applyFoldersSerially and applyIncrementalChanges to utilize the new timeout wrapper.
- Removed redundant logging and error handling code related to timeout in favor of centralized handling in wrapWithTimeout.
- Adjusted test expectations to reflect changes in error reporting for context deadlines.

---------

Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 09:07:04 +00:00
Gareth
0b46123300 Tempo: remove backend migration feature toggle (#116054)
* remove unused frontend code

* remove feature toggle definition

* fix tests
2026-01-12 17:21:48 +09:00
Gabriel MABILLE
81b868ae91 grafana-iam: Split AuthZ apis feature toggle per apis (#116010)
* WIP: switched to feature toggles

* Add timeout
2026-01-12 09:00:51 +01:00
Hugo Häggmark
2a6a48ac39 chore: reduce Explore barrel files (#116051)
chore: Explore barrel files
2026-01-12 05:57:09 +01:00
grafana-pr-automation[bot]
f581a5a69b I18n: Download translations from Crowdin (#116094)
New Crowdin translations by GitHub Action

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-12 00:33:39 +00:00
Stephanie Hingtgen
5ff18c0802 Docs: Dashboards and folders: Add annotation and label restriction (#116049) 2026-01-09 21:53:56 +00:00
Jesse David Peterson
0c8c886930 PieChart: Fix right-oriented legends (#116084)
* chore(gdev-dashboard): reproduce CSS layout bug in a panel

* fix(table-legend): tweak CSS for right oriented table legend in piechart

* chore(gdev-dashboard): update migrated dashboard file to match
2026-01-09 21:07:35 +00:00
Will Assis
ba196958cd unified-storage: integration test to CRUD sql backend and sqlkv at the same time (#115907)
* add tests to check that we can CRUD resources to both backends at the same time
2026-01-09 15:38:13 -05:00
Adela Almasan
622c75af6d Suggestions: Add analytics (#115904) 2026-01-09 20:19:26 +00:00
Victor Cinaglia
e983aac141 Users: Fix "Last active" displaying incorrect value instead of "Never" (#115825) 2026-01-09 16:56:24 -03:00
Adela Almasan
c42421e616 Suggestions: Fix legend options for preview (#116078) 2026-01-09 13:31:56 -06:00
Yuri Tseretyan
e4ba1c1a6d Alerting: Refactor encryption and receiver handling for improved clarity (#115959)
* refactor: change GetReceiver to get by UID

to avoid conversions of name to uid back an forth

* refactor: consolidate all encrypt\decrypt functions

* move error from temporary location
2026-01-09 13:43:36 -05:00
Eric Shields
909ed02218 Scopes: Implement defaultPath support and refactor path resolution (#114863)
Implements support for the defaultPath field in Scope specifications

* Faster performance: Batch API call (fetchMultipleScopeNodes) replaces N sequential calls
* Instant selector opening: Pre-fetches all path nodes when applying scopes
* Consistent resolution: Single source of truth for scopeNodeId and parentNodeId across UI and URLs
* Correct URL syncing: scope_node parameter always reflects the canonical defaultPath
* Backwards compatible: Gracefully falls back when defaultPath is unavailable

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>
2026-01-09 09:26:02 -08:00
Matt Cowley
ad3763f04d UI: Use computed z-index for UsersIndicator to fix tab order (#115894)
* Use computed z-index for UsersIndicator to fix tab order

* Apply z-index from UsersIndicator via nth-of-type
2026-01-09 16:56:48 +00:00
Ashley Harrison
ca6ab973b4 Modal/Drawer: Switch to use floating-ui's focus trapping (#116017)
* Add awareness of a parent when toggletip is rendered to work inside other modals

* switch modal + drawer to use floating-ui's focus trapping

* remove outdated docs

* fix some unit tests

* fix scopes tests

* remove duplicate aria-label

* kick CI

* fix e2e tests

---------

Co-authored-by: tdbishop <thomas.bishop@grafana.com>
2026-01-09 16:47:25 +00:00
Galen Kistler
9cd811b9e6 LogsDrilldownDefaultColumns: Upgrade API from alpha to beta (#116035)
* chore: release logsdrilldown default columns v1beta1
2026-01-09 14:28:29 +01:00
Matheus Macabu
52cd096d92 Secrets: Propagate ctx cancel and label it on decryptions (#116058) 2026-01-09 14:25:29 +01:00
Misi
98453fbcff IAM: Use the new way to authorize resources (#116061)
* Use name for authz for User, SA, Team

* Use VerbList
2026-01-09 14:21:20 +01:00
Renato Costa
ccdafc3fb2 unified-storage: fix event persistence when sqlkv is enabled (#116033) 2026-01-09 07:58:58 -05:00
Paul Marbach
12abbd5a15 Sparkline: Hide axes for real (#116040) 2026-01-09 07:57:56 -05:00
Rafael Bortolon Paulovic
1c5caeb987 fix(unified): err on timeout to open index (#115953)
* fix(unified): default index path to ephemeral storage mount path

Signed-off-by: Rafael Paulovic <rafael.paulovic@grafana.com>

* Revert "fix: use memory index if index file already open (#115720)"

This reverts commit dc4c106e91.

* fix(unified): set index_path for tests

* chore(unified): re-add bolt open timeout and test for error handling

* chore(unified): return err on timeout

* chore(unified): revert changes to default, use DataPath if index_path not set

* chore(unified): add defaults.ini entry for unified_storage

This is needed to override using env. vars

* chore: revert unrelated diff

* chore: address code review comments

- reduce bolt timeout to 1s
- remove errIndexLocked err type
- add more information about index_path in defaults.ini

---------

Signed-off-by: Rafael Paulovic <rafael.paulovic@grafana.com>
2026-01-09 13:48:54 +01:00
Ashley Harrison
71a65e1f80 Custom branding: Correctly override bouncing loader (#115871)
use the custom branding logo for the bouncing loader
2026-01-09 11:56:55 +00:00
Ashley Harrison
ec12176220 Chore: Bump storybook to fix CVE (#115927)
* bump storybook to fix CVE

* reapply patch
2026-01-09 11:56:29 +00:00
Stephanie Hingtgen
0cf4f7c4de Library Elements: Deprecate folderFilter query param; update docs for folderFilterUIDs (#116048) 2026-01-09 04:24:18 -07:00
Stephanie Hingtgen
b0785e506f Dashboard Tags: Validate max length (#116047) 2026-01-09 03:57:39 -07:00
Stephanie Hingtgen
5f8668b3aa Preferences: Add API validation and update documentation (#116045) 2026-01-09 03:57:15 -07:00
Will Browne
368762c026 Plugins: Add plugins module (#115951)
* create plugins go module

* make update-workspace

* ref from plugins app

* undo README change

* fix Dockerfile

* make update-workspace

* re-add plugins/codegen
2026-01-09 10:33:56 +00:00
Matheus Macabu
a56fa3c7b5 Revert "Secrets: Remove unused register_api_server setting" (#116004)
Revert "Secrets: Remove unused register_api_server setting (#113849)"

This reverts commit 4ee2112ea4.
2026-01-09 11:01:46 +01:00
Alexander Zobnin
f5f9a66fa8 Zanzana: Instrument legacy reconciler (#116018) 2026-01-09 10:16:06 +01:00
Roberto Jiménez Sánchez
eb6c22af36 Provisioning: Add connection operator with health check updates (#116028)
* Add connection operator with health check updates

- Add ConnectionController to watch and reconcile Connection resources
- Add ConnectionStatusPatcher for updating connection status
- Add connection_operator.go entry point for standalone operator
- Register connection operator in pkg/operators/register.go
- Add connection controller to in-process setup in register.go
- Add unit tests for connection controller
- Add integration tests for health check updates

* Fix integration test: get latest version before update to avoid conflicts

* refactor: move repoFactory to operator-specific configs

- Remove repoFactory from shared provisioningControllerConfig
- Add repoFactory to repoControllerConfig and jobsControllerConfig
- This allows connection operator to run without repository setup

* Remove unneccesary comments
2026-01-09 09:08:49 +01:00
Oscar Kilhed
125cc5fddd Dashboard: Prevent changing layout to tabs when rows contain tabs (#116019)
- Add containsTabsLayout helper function to check if child layouts contain tabs
- Update DashboardLayoutSelector to disable tabs option when children contain tabs
- Show different tooltip message for parent vs child tabs nesting scenarios
- Add tests for the new functionality
2026-01-09 08:33:54 +01:00
grafana-pr-automation[bot]
45c25ab1d9 I18n: Download translations from Crowdin (#116046)
New Crowdin translations by GitHub Action

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-09 00:43:03 +00:00
Stephanie Hingtgen
7f34fae439 Zanzana: Run dashboard integration tests backed by zanzana (#115771) 2026-01-08 21:51:42 +00:00
Will Assis
f028b9dbdb unified-storage: Sql kv compat tests fields check (#115891)
* add tests to check that resource_history, resource and resource_version are being populated properly with sqlkv
2026-01-08 15:58:32 -05:00
Gabriel MABILLE
e95f8bf843 grafana-iam: Split UpdateAPIGroupInfo in multiple resource specific functions. (#116037)
* OnGoing fixing cyclomatic complexity

* Reduce cyclo complexity

* Spaces
2026-01-08 21:50:44 +01:00
Will Assis
f669bc4448 unified-storage: refactor Sql backend and sqlkv compat tests (#115849)
* move sql and sqlkv backends compatibility tests

* refactor compatibility tests

* run storage backend tests with and without rvmanager

* fix

* fix

* fmt

* fix

* address feedback

* fmt
2026-01-08 15:06:44 -05:00
Haris Rozajac
a79cda3328 Dashboard Conversion: Handle legacy string ds ref in panel queries datasources in V1-> V2 conversion (#116032) 2026-01-08 12:17:03 -07:00
alexandra vargas
c7986976e4 Fix issue with remote prometheus api 2026-01-08 17:38:04 +01:00
Gilles De Mey
65cdf6cd45 Alerting: Align redux toolkit versions (#116016) 2026-01-08 17:26:33 +01:00
Roberto Jiménez Sánchez
7be93d9af4 Provisioning: add /connections/{name}/repositories endpoint (#116020)
* feat(provisioning): add /connections/{name}/repositories endpoint

Add a new subresource endpoint to list external repositories from git
providers (GitHub, GitLab, Bitbucket) accessible through a connection.

Changes:
- Add ExternalRepositoryList and ExternalRepository types with Name, Owner, and URL fields
- Create connection_repositories.go connector (returns 'not implemented' for now)
- Register storage and authorization for the repositories subresource
- Update OpenAPI documentation
- Regenerate code (deepcopy, openapi, client)

The endpoint is accessible at /apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/connections/{name}/repositories
and requires admin read access.

Related: #TBD

* test(provisioning): add unit and integration tests for connection repositories endpoint

- Add unit tests for connection_repositories connector
- Add integration tests for authorization and endpoint behavior
- Tests verify not implemented response and proper authorization

* Fix generation

* fix(tests): fix test compilation and assertions

- Remove unused import in unit test
- Fix integration test Raw() usage
- Fix ExternalRepositoryList type verification test

* Format code

* fix(provisioning): fix ineffectual assignment in connection_repositories connector

- Add debug log statement to use logger variable
- Fixes linter error about ineffectual assignment to ctx
2026-01-08 16:14:19 +00:00
Jo
347075bffe docs: update anonymous access docs (#116011)
* docs: update anonymous access docs

* reset title

* reset title
2026-01-08 16:57:34 +01:00
Larissa Wandzura
0db188e95d Docs: Added a Graphite troubleshooting guide (#115971)
* added a troubleshooting guide

* spelling fix

* fixed linter issue
2026-01-08 15:41:50 +00:00
Tom Ratcliffe
f38df468b5 Chore: Remove unifiedHistory feature toggle and associated code (#113857) 2026-01-08 15:25:49 +00:00
JsEnthusiast
c78c2d7231 Security: Remove unused Bootstrap v2.3.2 vendor files (#114339)
Removes Bootstrap v2.3.2 files that are not used in the codebase
but are flagged by security vulnerability scanners.

Changes:
- Removed public/vendor/bootstrap/ directory
- Removed public/vendor/tagsinput/bootstrap-tagsinput.js
- Removed .bootstrap-tagsinput CSS block from public/sass/_angular.scss

These files were replaced by modern React components during the
Angular to React migration. The TagsInput functionality is now
provided by packages/grafana-ui/src/components/TagsInput/TagsInput.tsx.

Bootstrap v2.3.2 (from 2013) has known CVEs but poses no actual risk
since the files are not loaded or executed. This change eliminates
false-positive security scan alerts.

Evidence:
- No import statements found for these files
- No script tags loading bootstrap.js
- No webpack bundling of vendor files
- Modern React TagsInput component in use
- Last modified: June 2022 (security patch only)
2026-01-08 15:23:32 +00:00
Haris Rozajac
8f4fa9ed05 ExportAsCode: Use layout creator when exporting v1 dashboard as v2 (#115754)
* Alt to #115457

* fix tests

* Remove exports

* skip scene creation options for template route

---------

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
2026-01-08 08:12:02 -07:00
Alexander Zobnin
0aae7e01bc Zanzana: Add remote client metrics (#116012)
* Zanzana: Add remote client metrics

* fix linter
2026-01-08 15:24:54 +01:00
alexandra vargas
e4009a42a1 fix issue with raw results in the modal 2026-01-08 14:41:27 +01:00
Will Assis
58e9e4a56d unified-storage: fixes for sqlkv to work with postgres (#115961)
* unified-storage: fixes for sqlkv to work with postgres
2026-01-08 08:21:35 -05:00
alexandra vargas
06d11d739b Handle datasource variables in validator query grouping 2026-01-08 14:20:47 +01:00
Matheus Macabu
dff9bea3e8 Reporting: Add feature toggle for CSV encoding options (#115584) 2026-01-08 13:56:54 +01:00
Galen Kistler
19cfab89f3 Explore: Traces query that will work with either logs drilldown or explore (#115837)
* fix: use query that will work with either logs drilldown or explore
2026-01-08 06:55:01 -06:00
Mustafa Sencer Özcan
088bab8b38 feat: enable auto migration based on resource count (#115619)
* feat(unified): migration at startup based on resource count

-- draft

* feat: introduce auto migration enablement for dashboards & folders

* feat: enable auto migration based on threshold

* fix: improve

* fix: pass in the auto migrate per migration definition

* fix: minor

* fix: only use one options

* fix: test

* fix: test

* fix: tests

* fix: simplify configs

* chore: rename

* fix: add integration test

* fix: add integration test

* fix: integration tests

* chore: add comments

* fix: address comment

* fix: address comments

* fix: test and auto migration flow

* fix: test

---------

Co-authored-by: Rafael Paulovic <rafael.paulovic@grafana.com>
2026-01-08 13:30:40 +01:00
Sonia Aguilar
9e8bdee283 Alerting: Hide DMA options when no manageAlerts datasources exist (#115952)
* hide data source managed options in the more menu in the list view

* Hide type selector in the new alert form when no data source has mangeAlerts enabled
2026-01-08 13:17:37 +01:00
Gilles De Mey
bb5bb00e4d Alerting: Rename alerts to alert activitity (#115948)
rename alerts to alert activitity
2026-01-08 11:48:27 +01:00
Misi
5fcc67837a IAM: Update ExternalGroupMapping authorizer (#115627)
* wip

* Add target resource authorizer to ExternalGroupMapping

* Regenerate OpenAPI snapshot

* Update pkg/registry/apis/iam/authorizer/external_group_mapping.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update pkg/registry/apis/iam/register.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address feedback, reorganize

* Add tests to the public interface separately

* Address feedback

* Address feedback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 11:47:00 +01:00
renovate-sh-app[bot]
79f2016a66 chore(deps): update dependency @openfeature/ofrep-web-provider to v0.3.5 (#115963)
| datasource | package                         | from  | to    |
| ---------- | ------------------------------- | ----- | ----- |
| npm        | @openfeature/ofrep-web-provider | 0.3.3 | 0.3.5 |

Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
2026-01-08 09:49:04 +00:00
renovate-sh-app[bot]
7858dcb9c1 chore(deps): update dependency @openfeature/web-sdk to v1.7.2 (#115964)
| datasource | package              | from  | to    |
| ---------- | -------------------- | ----- | ----- |
| npm        | @openfeature/web-sdk | 1.7.1 | 1.7.2 |

Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
2026-01-08 09:48:41 +00:00
Joey
27eb488a96 Chore: Remove Drilldown Investigations (#115471)
* Remove investigations app

* Remove other files

* Remove feature toggle

* Update codeowners

* make update-workspace

* Regen files with make gen-go gen-feature-toggles
2026-01-08 09:28:20 +00:00
grafana-pr-automation[bot]
97af86efb2 I18n: Download translations from Crowdin (#115968)
New Crowdin translations by GitHub Action

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-08 00:43:13 +00:00
Paul Marbach
f58ab2a6a1 Gauge: Fix endpoint rendering for non-gradient cases (#115910)
* Gauge: Fix endpoint rendering for non-gradient cases

* break out the endpoint markers to its own component with tests
2026-01-07 17:17:35 -05:00
Charandas
b96a1ae722 Custom Routes: use existing server's mux container instead of gorilla.Mux (#115605) 2026-01-07 12:46:27 -08:00
Kim Nylander
a53875e621 [DOC] Changed so max_spans_per_span_set can't be changed in Cloud Traces (#115914)
Changed so max_spans_per_span_set can't be changed in Cloud Traces
2026-01-07 15:46:02 -05:00
Cory Forseth
9598ae6434 Datasources: extract data source read methods from service (#115834)
* extra data source read methods

* update tests

* more tests

* fix more tests; actually initialize retriever instead of sending nil

* moving GetAllDataSources isn't strictly required, so keep to minimal changes

* better name for retriever logger

Co-authored-by: Dafydd <72009875+dafydd-t@users.noreply.github.com>

* add compile-time check for DS retriever impl

---------

Co-authored-by: Dafydd <72009875+dafydd-t@users.noreply.github.com>
Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
2026-01-07 14:29:59 -06:00
owensmallwood
ab0b05550f Unified Storag: Fix readme (#115957)
* fix readme

* spelling
2026-01-07 19:35:33 +00:00
beejeebus
4518add556 Use a different metric name for new config CRUD APIs
Also, make sure to register the metrics with the same prometheus registerer
as the http server, so that metrics will show up.
2026-01-07 14:28:31 -05:00
Kristina Demeshchik
00b89b0d29 Dashboards: Fix liveNow not working for panels with time shift (#115902)
* relative time for timeshifts

* remove extra assertion

* absolute time range
2026-01-07 14:24:20 -05:00
Todd Treece
a3eedfeb73 Plugins: Move fixed role registration behind toggle (#115940) 2026-01-07 13:52:01 -05:00
Renato Costa
1e8f1f74ea unified-storage: apply backwards compatibility changes outside sqlkv (#115954) 2026-01-07 13:51:15 -05:00
owensmallwood
66b05914e2 Tracing: Use service name from config (#115955)
use service name from config
2026-01-07 12:50:11 -06:00
Yunwen Zheng
0c60d356d1 RecentlyViewedDashboards: Hide entire section when there is no recently view item (#115905)
* RecentlyViewedDashboards: Hide entire section when there is no recently view item
2026-01-07 13:31:48 -05:00
Ezequiel Victorero
41d7213d7e Docs: Update dualwrite ini config (#115934) 2026-01-07 17:58:58 +01:00
Todd Treece
efad6c7be0 Chore: Update enterprise imports (#115947) 2026-01-07 16:55:59 +00:00
alexandra vargas
74548dbb73 fix api endpoint witht correct path, using getApiNamespace 2026-01-07 17:40:34 +01:00
Paulo Dias
e116254f32 Alerting: Update createdBy field when silence is being Recreated (#115543) 2026-01-07 16:05:53 +00:00
alexandra vargas
751a399b03 Wire up compatibility check to CommunityDashboardSection 2026-01-07 16:02:57 +01:00
alexandra vargas
c9e044b2c7 Add 'Check Compatibility' button to DashboardCard" 2026-01-06 15:41:26 +01:00
alexandra vargas
92041e5a05 Create compatibility modal for mvp 2026-01-06 15:10:40 +01:00
alexandra vargas
6ee1a6ea7f remove file mistake 2026-01-06 14:10:42 +01:00
alexandra vargas
4f66b1df5a Implemented TypeScript API client for calling the dashboard validator backend from the Grafana frontend. 2026-01-06 14:09:29 +01:00
alexandra vargas
01f959be97 Improve error handling for validator
- Surface error codes for datasource possible errors (not found, unreachabel, auth, timeout)
2026-01-06 13:27:34 +01:00
alexandra vargas
f81deced02 Merge branch 'axelav/dash-validator-app-mvp' into axelav/dash-validator-app-prometheus-poc 2026-01-06 10:50:39 +01:00
alexandra vargas
ca3bce54a8 Fix go workspace 2026-01-06 10:50:07 +01:00
alexandra vargas
3d3eeb4472 Merge remote-tracking branch 'origin' into axelav/dash-validator-app-mvp 2026-01-06 10:49:27 +01:00
alexandra vargas
2b6e2c5737 Merge branch 'axelav/dash-validator-app-mvp' into axelav/dash-validator-app-prometheus-poc 2026-01-05 16:55:59 +01:00
alexandra vargas
306aee16a5 Fix codeowners 2026-01-05 16:55:33 +01:00
alexandra vargas
8319f62ef4 Merge branch 'axelav/dash-validator-app-mvp' into axelav/dash-validator-app-prometheus-poc 2026-01-05 15:54:10 +01:00
alexandra vargas
b8b792f78a Fix linting 2026-01-05 15:53:23 +01:00
alexandra vargas
8a0f2fa9f3 SuggestedDashboards- Extend dashvalidator POC: Prometheus validator 2025-12-31 17:06:49 +01:00
alexandra vargas
9d980a9244 Add 'check' endpoint to manifest, fix issue with grafana core depedencies 2025-12-31 15:14:46 +01:00
alexandra vargas
e442720cdc Wire dashvalidator app with grafana 2025-12-31 14:39:45 +01:00
alexandra vargas
e616d04010 Create app.go file and define app configuration, custom route and basic handler for that route 2025-12-31 13:48:48 +01:00
alexandra vargas
8e9675ce1c Create dashboard validator app
- Generate scaffolding
- Create DashboardCompatibilityScore Kind in CUE
2025-12-30 15:37:34 +01:00
772 changed files with 37117 additions and 13410 deletions

4
.github/CODEOWNERS vendored
View File

@@ -94,7 +94,6 @@
/apps/shorturl/ @grafana/sharing-squad
/apps/secret/ @grafana/grafana-operator-experience-squad
/apps/scope/ @grafana/grafana-operator-experience-squad
/apps/investigations/ @fcjack @matryer @svennergr
/apps/advisor/ @grafana/plugins-platform-backend
/apps/iam/ @grafana/access-squad
/apps/sdk.mk @grafana/grafana-app-platform-squad
@@ -102,6 +101,7 @@
/apps/example/ @grafana/grafana-app-platform-squad
/apps/logsdrilldown/ @grafana/observability-logs
/apps/annotation/ @grafana/grafana-backend-services-squad
/apps/dashvalidator/ @grafana/sharing-squad
/pkg/api/ @grafana/grafana-backend-group
/pkg/apis/ @grafana/grafana-app-platform-squad
/pkg/apis/query @grafana/grafana-datasources-core-services
@@ -1191,6 +1191,7 @@ embed.go @grafana/grafana-as-code
/pkg/registry/apps/advisor @grafana/plugins-platform-backend
/pkg/registry/apps/alerting @grafana/alerting-backend
/pkg/registry/apps/plugins @grafana/plugins-platform-backend
/pkg/registry/apps/dashvalidator @grafana/sharing-squad
/pkg/codegen/ @grafana/grafana-as-code
/pkg/codegen/generators @grafana/grafana-as-code
/pkg/kinds/*/*_gen.go @grafana/grafana-as-code
@@ -1276,6 +1277,7 @@ embed.go @grafana/grafana-as-code
/.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform
/.github/workflows/i18n-crowdin-create-tasks.yml @grafana/grafana-frontend-platform
/.github/workflows/i18n-verify.yml @grafana/grafana-frontend-platform
/.github/workflows/deploy-storybook.yml @grafana/grafana-frontend-platform
/.github/workflows/deploy-storybook-preview.yml @grafana/grafana-frontend-platform
/.github/workflows/scripts/crowdin/create-tasks.ts @grafana/grafana-frontend-platform
/.github/workflows/scripts/publish-frontend-metrics.mts @grafana/grafana-frontend-platform

View File

@@ -14,6 +14,9 @@ outputs:
frontend:
description: Whether the frontend or self has changed in any way
value: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}
frontend-packages:
description: Whether any frontend packages have changed
value: ${{ steps.changed-files.outputs.frontend_packages_any_changed || 'true' }}
e2e:
description: Whether the e2e tests or self have changed in any way
value: ${{ steps.changed-files.outputs.e2e_any_changed == 'true' ||
@@ -97,6 +100,12 @@ runs:
- '.yarn/**'
- 'apps/dashboard/pkg/migration/**'
- '${{ inputs.self }}'
frontend_packages:
- '.github/actions/checkout/**'
- '.github/actions/change-detection/**'
- 'packages/**'
- './scripts/validate-npm-packages.sh'
- '${{ inputs.self }}'
e2e:
- 'e2e/**'
- 'e2e-playwright/**'
@@ -153,6 +162,8 @@ runs:
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"
echo "Frontend: ${{ steps.changed-files.outputs.frontend_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.frontend_all_changed_files }}"
echo "Frontend packages: ${{ steps.changed-files.outputs.frontend_packages_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.frontend_packages_all_changed_files }}"
echo "E2E: ${{ steps.changed-files.outputs.e2e_any_changed || 'true' }}"
echo " --> ${{ steps.changed-files.outputs.e2e_all_changed_files }}"
echo " --> ${{ steps.changed-files.outputs.backend_all_changed_files }}"

View File

@@ -4,8 +4,8 @@ description: Sets up a node.js environment with presets for the Grafana reposito
runs:
using: "composite"
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
cache-dependency-path: 'yarn.lock'

View File

@@ -19,7 +19,6 @@ updates:
- "/apps/dashboard"
- "/apps/folder"
- "/apps/iam"
- "/apps/investigations"
- "/apps/playlist"
- "/apps/plugins"
- "/apps/preferences"

79
.github/workflows/deploy-storybook.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: Deploy Storybook
on:
workflow_dispatch:
# push:
# branches:
# - main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
detect-changes:
# Only run in grafana/grafana
if: github.repository == 'grafana/grafana'
name: Detect whether code changed
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
changed-frontend-packages: ${{ steps.detect-changes.outputs.frontend-packages }}
steps:
- uses: actions/checkout@v5
with:
persist-credentials: true # required to get more history in the changed-files action
fetch-depth: 2
- name: Detect changes
id: detect-changes
uses: ./.github/actions/change-detection
with:
self: .github/workflows/deploy-storybook.yml
deploy-storybook:
name: Deploy Storybook
runs-on: ubuntu-latest
needs: detect-changes
# Only run in grafana/grafana
if: github.repository == 'grafana/grafana' && needs.detect-changes.outputs.changed-frontend-packages == 'true'
permissions:
contents: read
id-token: write
env:
BUCKET_NAME: grafana-storybook
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Install dependencies
run: yarn install --immutable
- name: Build storybook
run: yarn storybook:build
# Create the GCS folder name
# Right now, this just returns "canary"
# But we'll expand this to work for "latest" as well in the future
- name: Create deploy name
id: create-deploy-name
run: |
echo "deploy-name=canary" >> "$GITHUB_OUTPUT"
- name: Upload Storybook
uses: grafana/shared-workflows/actions/push-to-gcs@main
with:
environment: prod
bucket: ${{ env.BUCKET_NAME }}
bucket_path: ${{ steps.create-deploy-name.outputs.deploy-name }}
path: packages/grafana-ui/dist/storybook
service_account: github-gf-storybook-deploy@grafanalabs-workload-identity.iam.gserviceaccount.com
parent: false

View File

@@ -17,6 +17,7 @@ jobs:
outputs:
changed: ${{ steps.detect-changes.outputs.frontend }}
prettier: ${{ steps.detect-changes.outputs.frontend == 'true' || steps.detect-changes.outputs.docs == 'true' }}
changed-frontend-packages: ${{ steps.detect-changes.outputs.frontend-packages }}
steps:
- uses: actions/checkout@v5
with:
@@ -42,11 +43,8 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Node
uses: ./.github/actions/setup-node
- run: yarn install --immutable --check-cache
- run: yarn run prettier:check
- run: yarn run lint
@@ -63,11 +61,8 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Setup Enterprise
uses: ./.github/actions/setup-enterprise
with:
@@ -89,11 +84,8 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Node
uses: ./.github/actions/setup-node
- run: yarn install --immutable --check-cache
- run: yarn run typecheck
lint-frontend-typecheck-enterprise:
@@ -109,11 +101,8 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Setup Enterprise
uses: ./.github/actions/setup-enterprise
with:
@@ -133,11 +122,8 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Node
uses: ./.github/actions/setup-node
- run: yarn install --immutable --check-cache
- name: Generate API clients
run: |
@@ -164,11 +150,8 @@ jobs:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Setup Enterprise
uses: ./.github/actions/setup-enterprise
with:
@@ -187,3 +170,26 @@ jobs:
echo "${uncommited_error_message}"
exit 1
fi
lint-frontend-packed-packages:
needs: detect-changes
permissions:
contents: read
id-token: write
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.changed-frontend-packages == 'true'
name: Verify packed frontend packages
runs-on: ubuntu-latest
steps:
- name: Checkout build commit
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Install dependencies
run: yarn install --immutable
- name: Build and pack packages
run: |
yarn run packages:build
yarn run packages:pack
- name: Validate packages
run: ./scripts/validate-npm-packages.sh

View File

@@ -67,14 +67,6 @@ linters:
deny:
- pkg: github.com/grafana/grafana/pkg
desc: apiserver is not allowed to import grafana core
apps-investigation:
list-mode: lax
files:
- ./apps/investigations/*
- ./apps/investigations/**/*
deny:
- pkg: github.com/grafana/grafana/pkg
desc: apps/investigations is not allowed to import grafana core
apps-playlist:
list-mode: lax
files:

View File

@@ -1,8 +1,8 @@
diff --git a/dist/builder-manager/index.js b/dist/builder-manager/index.js
index 3d7f9b213dae1801bda62b31db31b9113e382ccd..212501c63d20146c29db63fb0f6300c6779eecb5 100644
index ac8ac6a5f6a3b7852c4064e93dc9acd3201289e6..34a0a5a5c38dd7fe525c9ebd382a10a451d4d4f3 100644
--- a/dist/builder-manager/index.js
+++ b/dist/builder-manager/index.js
@@ -1970,7 +1970,7 @@ var pa = /^\/($|\?)/, G, C, xt = /* @__PURE__ */ o(async (e) => {
@@ -1974,7 +1974,7 @@ var pa = /^\/($|\?)/, G, C, xt = /* @__PURE__ */ o(async (e) => {
bundle: !0,
minify: !0,
sourcemap: !1,

View File

@@ -91,6 +91,7 @@ COPY pkg/storage/unified/resource pkg/storage/unified/resource
COPY pkg/storage/unified/resourcepb pkg/storage/unified/resourcepb
COPY pkg/storage/unified/apistore pkg/storage/unified/apistore
COPY pkg/semconv pkg/semconv
COPY pkg/plugins pkg/plugins
COPY pkg/aggregator pkg/aggregator
COPY apps/playlist apps/playlist
COPY apps/quotas apps/quotas
@@ -103,10 +104,10 @@ COPY apps/collections apps/collections
COPY apps/provisioning apps/provisioning
COPY apps/secret apps/secret
COPY apps/scope apps/scope
COPY apps/investigations apps/investigations
COPY apps/logsdrilldown apps/logsdrilldown
COPY apps/advisor apps/advisor
COPY apps/dashboard apps/dashboard
COPY apps/dashvalidator apps/dashvalidator
COPY apps/folder apps/folder
COPY apps/iam apps/iam
COPY apps apps

View File

@@ -135,7 +135,7 @@ i18n-extract-enterprise:
@echo "Skipping i18n extract for Enterprise: not enabled"
else
i18n-extract-enterprise:
@echo "Extracting i18n strings for Enterprise"
@echo "Extracting i18n strings for Enterprise"
cd public/locales/enterprise && yarn run i18next-cli extract --sync-primary
endif
@@ -227,6 +227,10 @@ fix-cue:
gen-jsonnet:
go generate ./devenv/jsonnet
.PHONY: gen-themes
gen-themes:
go generate ./pkg/services/preference
.PHONY: update-workspace
update-workspace: gen-go
@echo "updating workspace"
@@ -244,6 +248,7 @@ build-go-fast: ## Build all Go binaries without updating workspace.
.PHONY: build-backend
build-backend: ## Build Grafana backend.
@echo "build backend"
$(MAKE) gen-themes
$(GO) run build.go $(GO_BUILD_FLAGS) build-backend
.PHONY: build-air

View File

@@ -4,7 +4,7 @@ go 1.25.5
require (
github.com/go-kit/log v0.2.1
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
github.com/grafana/grafana-app-sdk v0.48.7
github.com/grafana/grafana-app-sdk/logging v0.48.7

View File

@@ -243,8 +243,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopKKNhDavCLgehw60jdtl/sIxdfzmVts=
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f h1:3bXOyht68qkfvD6Y8z8XoenFbytSSOIkr/s+AqRzj0o=
github.com/grafana/alerting v0.0.0-20260112172717-98a49ed9557f/go.mod h1:Ji0SfJChcwjgq8ljy6Y5CcYfHfAYKXjKYeysOoDS/6s=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=

View File

@@ -0,0 +1,287 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v1beta1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"panels": [
{
"datasource": "${datasource}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [10, 10],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 0
},
"id": 16,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true,
"refId": "B"
}
],
"title": "Number of Alloy Instances",
"type": "timeseries"
},
{
"datasource": "${datasource}",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percentunit"
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 0
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"hide": true,
"instant": false,
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
},
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "CPU usage",
"type": "timeseries"
}
],
"time": {
"from": "now-90m",
"to": "now"
},
"timezone": "utc",
"title": "Legacy DS Panel Query Ref",
"weekStart": ""
}
}

View File

@@ -852,6 +852,194 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "C",
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -914,6 +1102,24 @@
"name": "panel-6"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -879,6 +879,200 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"refId": "C",
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -973,6 +1167,32 @@
"name": "panel-6"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -0,0 +1,294 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v0alpha1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"panels": [
{
"datasource": "${datasource}",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [
10,
10
],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 0
},
"id": 16,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true,
"refId": "B"
}
],
"title": "Number of Alloy Instances",
"type": "timeseries"
},
{
"datasource": "${datasource}",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percentunit"
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 0
},
"id": 17,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": "${datasource}",
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"hide": true,
"instant": false,
"legendFormat": "{{instance}}",
"range": true,
"refId": "A"
},
{
"datasource": "${datasource}",
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "CPU usage",
"type": "timeseries"
}
],
"time": {
"from": "now-90m",
"to": "now"
},
"timezone": "utc",
"title": "Legacy DS Panel Query Ref",
"weekStart": ""
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -0,0 +1,405 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v2alpha1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"query": {
"kind": "grafana",
"spec": {}
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"builtIn": true,
"legacyOptions": {
"type": "dashboard"
}
}
}
],
"cursorSync": "Off",
"editable": true,
"elements": {
"panel-16": {
"kind": "Panel",
"spec": {
"id": 16,
"title": "Number of Alloy Instances",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "",
"spec": {
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true
}
},
"datasource": {
"type": "",
"uid": "${datasource}"
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "timeseries",
"spec": {
"pluginVersion": "",
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [
10,
10
],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
}
}
}
}
},
"panel-17": {
"kind": "Panel",
"spec": {
"id": 17,
"title": "CPU usage",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "",
"spec": {
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"instant": false,
"legendFormat": "{{instance}}",
"range": true
}
},
"datasource": {
"type": "",
"uid": "${datasource}"
},
"refId": "A",
"hidden": true
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "",
"spec": {
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true
}
},
"datasource": {
"type": "",
"uid": "${datasource}"
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "timeseries",
"spec": {
"pluginVersion": "",
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-16"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-17"
}
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [],
"timeSettings": {
"timezone": "utc",
"from": "now-90m",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "Legacy DS Panel Query Ref",
"variables": []
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -0,0 +1,411 @@
{
"kind": "Dashboard",
"apiVersion": "dashboard.grafana.app/v2beta1",
"metadata": {
"name": "legacy-ds-ref"
},
"spec": {
"annotations": [
{
"kind": "AnnotationQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "grafana",
"version": "v0",
"datasource": {
"name": "-- Grafana --"
},
"spec": {}
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"builtIn": true,
"legacyOptions": {
"type": "dashboard"
}
}
}
],
"cursorSync": "Off",
"editable": true,
"elements": {
"panel-16": {
"kind": "Panel",
"spec": {
"id": 16,
"title": "Number of Alloy Instances",
"description": "",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "",
"version": "v0",
"datasource": {
"name": "${datasource}"
},
"spec": {
"editorMode": "code",
"expr": "count by (version) (alloy_build_info{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\"})",
"instant": false,
"legendFormat": "{{version}}",
"range": true
}
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "timeseries",
"version": "",
"spec": {
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Minimum cluster size"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "red",
"mode": "fixed"
}
},
{
"id": "custom.lineStyle",
"value": {
"dash": [
10,
10
],
"fill": "dash"
}
},
{
"id": "custom.lineWidth",
"value": 1
}
]
}
]
}
}
}
}
},
"panel-17": {
"kind": "Panel",
"spec": {
"id": 17,
"title": "CPU usage",
"description": "CPU usage of the Alloy process relative to 1 CPU core.\n\nFor example, 100% means using one entire CPU core.\n",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "",
"version": "v0",
"datasource": {
"name": "${datasource}"
},
"spec": {
"expr": "rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval])\n",
"instant": false,
"legendFormat": "{{instance}}",
"range": true
}
},
"refId": "A",
"hidden": true
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "",
"version": "v0",
"datasource": {
"name": "${datasource}"
},
"spec": {
"editorMode": "code",
"expr": "sum(rate(alloy_resources_process_cpu_seconds_total{cluster=~\"$cluster\", namespace=~\"$namespace\", job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))",
"instant": false,
"legendFormat": "Total",
"range": true
}
},
"refId": "B",
"hidden": false
}
}
],
"transformations": [],
"queryOptions": {}
}
},
"vizConfig": {
"kind": "VizConfig",
"group": "timeseries",
"version": "",
"spec": {
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"timeCompare": false,
"tooltip": {
"hideZeros": false,
"mode": "single",
"sort": "none"
}
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": 0,
"color": "green"
},
{
"value": 80,
"color": "red"
}
]
},
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"showValues": false,
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
}
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Total"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": true,
"viz": true
}
}
]
}
]
}
}
}
}
}
},
"layout": {
"kind": "GridLayout",
"spec": {
"items": [
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-16"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 0,
"width": 8,
"height": 9,
"element": {
"kind": "ElementReference",
"name": "panel-17"
}
}
}
]
}
},
"links": [],
"liveNow": false,
"preload": false,
"tags": [],
"timeSettings": {
"timezone": "utc",
"from": "now-90m",
"to": "now",
"autoRefresh": "",
"autoRefreshIntervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"hideTimepicker": false,
"fiscalYearStartMonth": 0
},
"title": "Legacy DS Panel Query Ref",
"variables": []
},
"status": {
"conversion": {
"failed": false,
"storedVersion": "v1beta1"
}
}
}

View File

@@ -711,6 +711,146 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 18
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 18
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -711,6 +711,146 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 18
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 9,
"w": 8,
"x": 8,
"y": 18
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -879,6 +879,200 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "DataQuery",
"group": "datasource",
"version": "v0",
"datasource": {
"name": "-- Dashboard --"
},
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"refId": "C",
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -941,6 +1135,24 @@
"name": "panel-6"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "AutoGridLayoutItem",
"spec": {
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -711,6 +711,146 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 0,
"y": 6
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 8,
"y": 6
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -711,6 +711,146 @@
],
"title": "Mixed DS WITHOUT REFS",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"description": "Panel with a single -- Dashboard -- datasource query",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 0,
"y": 6
},
"id": 7,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
}
],
"title": "Single Dashboard DS Query",
"type": "stat"
},
{
"datasource": {
"type": "mixed",
"uid": "-- Mixed --"
},
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
}
}
},
"gridPos": {
"h": 3,
"w": 8,
"x": 8,
"y": 6
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "12.1.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 1,
"refId": "A",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "B",
"withTransforms": true
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 3,
"refId": "C",
"withTransforms": true
}
],
"title": "Multiple Dashboard DS Queries",
"type": "stat"
}
],
"preload": false,

View File

@@ -852,6 +852,194 @@
}
}
}
},
"panel-7": {
"kind": "Panel",
"spec": {
"id": 7,
"title": "Single Dashboard DS Query",
"description": "Panel with a single -- Dashboard -- datasource query",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
},
"panel-8": {
"kind": "Panel",
"spec": {
"id": 8,
"title": "Multiple Dashboard DS Queries",
"description": "Panel with multiple -- Dashboard -- datasource queries (should be mixed)",
"links": [],
"data": {
"kind": "QueryGroup",
"spec": {
"queries": [
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 1,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "A",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 2,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "B",
"hidden": false
}
},
{
"kind": "PanelQuery",
"spec": {
"query": {
"kind": "datasource",
"spec": {
"panelId": 3,
"withTransforms": true
}
},
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"refId": "C",
"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"
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
}
}
}
}
}
},
"layout": {
@@ -946,6 +1134,32 @@
"name": "panel-6"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 0,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-7"
}
}
},
{
"kind": "GridLayoutItem",
"spec": {
"x": 8,
"y": 6,
"width": 8,
"height": 3,
"element": {
"kind": "ElementReference",
"name": "panel-8"
}
}
}
]
}

View File

@@ -88,6 +88,11 @@ func ConvertDashboard_V0_to_V1beta1(in *dashv0.Dashboard, out *dashv1.Dashboard,
// Which means that we have schemaVersion: 42 dashboards where datasource variable references are still strings
normalizeTemplateVariableDatasources(out.Spec.Object)
// Normalize panel and target datasources from string to object format
// This handles legacy dashboards where panels/targets have datasource: "$datasource" (string)
// instead of datasource: { uid: "$datasource" } (object)
normalizePanelDatasources(out.Spec.Object)
return nil
}
@@ -134,3 +139,62 @@ func isTemplateVariableRef(s string) bool {
}
return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "${")
}
// normalizePanelDatasources converts panel and target string datasources to object format.
// Legacy dashboards may have panels/targets with datasource: "$datasource" (string).
// This normalizes them to datasource: { uid: "$datasource" } for consistent V1→V2 conversion.
func normalizePanelDatasources(dashboard map[string]interface{}) {
panels, ok := dashboard["panels"].([]interface{})
if !ok {
return
}
normalizePanelsDatasources(panels)
}
// normalizePanelsDatasources normalizes datasources in a list of panels (including nested row panels)
func normalizePanelsDatasources(panels []interface{}) {
for _, panel := range panels {
panelMap, ok := panel.(map[string]interface{})
if !ok {
continue
}
// Handle row panels with nested panels
if panelType, _ := panelMap["type"].(string); panelType == "row" {
if nestedPanels, ok := panelMap["panels"].([]interface{}); ok {
normalizePanelsDatasources(nestedPanels)
}
}
// Normalize panel-level datasource
if ds := panelMap["datasource"]; ds != nil {
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
panelMap["datasource"] = map[string]interface{}{
"uid": dsStr,
}
}
}
// Normalize target-level datasources
targets, ok := panelMap["targets"].([]interface{})
if !ok {
continue
}
for _, target := range targets {
targetMap, ok := target.(map[string]interface{})
if !ok {
continue
}
if ds := targetMap["datasource"]; ds != nil {
if dsStr, ok := ds.(string); ok && isTemplateVariableRef(dsStr) {
targetMap["datasource"] = map[string]interface{}{
"uid": dsStr,
}
}
}
}
}
}

View File

@@ -2059,6 +2059,12 @@ func transformPanelQueries(ctx context.Context, panelMap map[string]interface{},
Uid: &dsUID,
}
}
} else if dsStr, ok := ds.(string); ok && isTemplateVariable(dsStr) {
// Handle legacy panel datasource as string (template variable reference e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
panelDatasource = &dashv2alpha1.DashboardDataSourceRef{
Uid: &dsStr,
}
}
}
@@ -2145,6 +2151,10 @@ func transformSingleQuery(ctx context.Context, targetMap map[string]interface{},
// Resolve Grafana datasource UID when type is "datasource" and UID is empty
queryDatasourceUID = resolveGrafanaDatasourceUID(queryDatasourceType, queryDatasourceUID)
}
} else if dsStr, ok := targetMap["datasource"].(string); ok && isTemplateVariable(dsStr) {
// Handle legacy target datasource as string (template variable reference e.g., "$datasource")
// Only process template variables - other string values are not supported in V2 format
queryDatasourceUID = dsStr
}
// Use panel datasource if target datasource is missing or empty

View File

@@ -1195,16 +1195,36 @@ func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, quer
// getPanelDatasource determines the panel-level datasource for V1.
// Returns:
// - Mixed datasource reference if queries use different datasources
// - Mixed datasource reference if multiple queries use Dashboard datasource (they fetch from different panels)
// - Dashboard datasource reference if a single query uses Dashboard datasource
// - First query's datasource if all queries use the same datasource
// - nil if no queries exist
// Compares based on V2 input without runtime resolution:
// - If query has explicit datasource.uid → use that UID and type
// - Else → use query.Kind as type (empty UID)
func getPanelDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} {
const sharedDashboardQuery = "-- Dashboard --"
if len(queries) == 0 {
return nil
}
// Count how many queries use Dashboard datasource
// Multiple dashboard queries need mixed mode because they fetch from different panels
// which may have different underlying datasources
dashboardDsQueryCount := 0
for _, query := range queries {
if query.Spec.Datasource != nil && query.Spec.Datasource.Uid != nil && *query.Spec.Datasource.Uid == sharedDashboardQuery {
dashboardDsQueryCount++
}
}
if dashboardDsQueryCount > 1 {
return map[string]interface{}{
"type": "mixed",
"uid": "-- Mixed --",
}
}
var firstUID, firstType string
var hasFirst bool
@@ -1239,6 +1259,16 @@ func getPanelDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[stri
}
}
// Handle case when a single query uses Dashboard datasource.
// This is needed for the frontend to properly activate and fetch data from source panels.
// See DashboardDatasourceBehaviour.tsx for more details.
if firstUID == sharedDashboardQuery {
return map[string]interface{}{
"type": "datasource",
"uid": sharedDashboardQuery,
}
}
// Not mixed - return the first query's datasource so the panel has a datasource set.
// This is required because the frontend's legacy PanelModel.PanelQueryRunner.run uses panel.datasource
// to resolve the datasource, and if undefined, it falls back to the default datasource

View File

@@ -290,7 +290,7 @@
],
"legend": {
"displayMode": "table",
"placement": "bottom",
"placement": "right",
"showLegend": true,
"values": [
"percent"
@@ -304,7 +304,7 @@
"fields": "",
"values": false
},
"showLegend": false,
"showLegend": true,
"strokeWidth": 1,
"text": {}
},
@@ -323,6 +323,15 @@
}
],
"title": "Percent",
"transformations": [
{
"id": "renameByRegex",
"options": {
"regex": "^Backend-(.*)$",
"renamePattern": "b-$1"
}
}
],
"type": "piechart"
},
{
@@ -366,7 +375,7 @@
],
"legend": {
"displayMode": "table",
"placement": "bottom",
"placement": "right",
"showLegend": true,
"values": [
"value"
@@ -380,7 +389,7 @@
"fields": "",
"values": false
},
"showLegend": false,
"showLegend": true,
"strokeWidth": 1,
"text": {}
},
@@ -399,6 +408,15 @@
}
],
"title": "Value",
"transformations": [
{
"id": "renameByRegex",
"options": {
"regex": "(.*)",
"renamePattern": "$1-how-much-wood-could-a-woodchuck-chuck-if-a-woodchuck-could-chuck-wood"
}
}
],
"type": "piechart"
},
{

View File

@@ -6,5 +6,4 @@ generate: install-app-sdk update-app-sdk
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--genoperatorstate=false \
--defencoding=none

View File

@@ -0,0 +1,228 @@
{
"kind": "CustomResourceDefinition",
"apiVersion": "apiextensions.k8s.io/v1",
"metadata": {
"name": "dashboardcompatibilityscores.dashvalidator.ext.grafana.com"
},
"spec": {
"group": "dashvalidator.ext.grafana.com",
"versions": [
{
"name": "v1alpha1",
"served": true,
"storage": true,
"schema": {
"openAPIV3Schema": {
"properties": {
"spec": {
"properties": {
"dashboardJson": {
"description": "Complete dashboard JSON object to validate.\nMust be a v1 dashboard schema (contains \"panels\" array).\nv2 dashboards (with \"elements\" structure) are not yet supported.",
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
},
"datasourceMappings": {
"description": "Array of datasources to validate against.\nThe validator will check dashboard queries against each datasource\nand provide per-datasource compatibility results.\n\nMVP: Only single datasource supported (array length = 1), Prometheus type only.\nFuture: Will support multiple datasources for dashboards with mixed queries.",
"items": {
"description": "DataSourceMapping specifies a datasource to validate dashboard queries against.\nMaps logical datasource references in the dashboard to actual datasource instances.",
"properties": {
"name": {
"description": "Optional human-readable name for display in results.\nIf not provided, UID will be used in error messages.\nExample: \"Production Prometheus (US-West)\"",
"type": "string"
},
"type": {
"description": "Type of datasource plugin.\nMVP: Only \"prometheus\" supported.\nFuture: \"mysql\", \"postgres\", \"elasticsearch\", etc.",
"type": "string"
},
"uid": {
"description": "Unique identifier of the datasource instance.\nExample: \"prometheus-prod-us-west\"",
"type": "string"
}
},
"required": ["uid", "type"],
"type": "object"
},
"type": "array"
}
},
"required": ["dashboardJson", "datasourceMappings"],
"type": "object"
},
"status": {
"properties": {
"additionalFields": {
"description": "additionalFields is reserved for future use",
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
},
"compatibilityScore": {
"description": "Overall compatibility score across all datasources (0-100).\nCalculated as: (total found metrics / total referenced metrics) * 100\n\nScore interpretation:\n- 100: Perfect compatibility, all queries will work\n- 80-99: Excellent, minor missing metrics\n- 50-79: Fair, significant missing metrics\n- 0-49: Poor, most queries will fail",
"type": "number"
},
"datasourceResults": {
"description": "Per-datasource validation results.\nArray length matches spec.datasourceMappings.\nEach element contains detailed metrics and query-level breakdown.",
"items": {
"description": "DataSourceResult contains validation results for a single datasource.\nProvides aggregate statistics and per-query breakdown of compatibility.",
"properties": {
"checkedQueries": {
"description": "Number of queries successfully validated.\nMay be less than totalQueries if some queries couldn't be parsed.",
"type": "integer"
},
"compatibilityScore": {
"description": "Overall compatibility score for this datasource (0-100).\nCalculated as: (foundMetrics / totalMetrics) * 100\nUsed to calculate the global compatibilityScore in status.",
"type": "number"
},
"foundMetrics": {
"description": "Number of metrics that exist in the datasource schema.\nfoundMetrics \u003c= totalMetrics",
"type": "integer"
},
"missingMetrics": {
"description": "Array of metric names that were referenced but don't exist.\nUseful for debugging why a dashboard shows \"no data\".\nExample for Prometheus: [\"http_requests_total\", \"api_latency_seconds\"]",
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"description": "Optional display name (matches DataSourceMapping.name if provided)",
"type": "string"
},
"queryBreakdown": {
"description": "Per-query breakdown showing which specific queries have issues.\nOne entry per query target (refId: \"A\", \"B\", \"C\", etc.) in each panel.\nAllows pinpointing exactly which panel/query needs fixing.",
"items": {
"description": "QueryBreakdown provides compatibility details for a single query within a panel.\nGranular per-query results allow users to identify exactly which queries need fixing.\n\nNote: A panel can have multiple queries (refId: \"A\", \"B\", \"C\", etc.),\nso there may be multiple QueryBreakdown entries for the same panelID.",
"properties": {
"compatibilityScore": {
"description": "Compatibility percentage for this individual query (0-100).\nCalculated as: (foundMetrics / totalMetrics) * 100\n100 = query will work perfectly, 0 = query will return no data.",
"type": "number"
},
"foundMetrics": {
"description": "Number of those metrics that exist in the datasource.\nfoundMetrics \u003c= totalMetrics",
"type": "integer"
},
"missingMetrics": {
"description": "Array of missing metric names specific to this query.\nHelps identify exactly which part of a query expression will fail.\nEmpty array means query is fully compatible.",
"items": {
"type": "string"
},
"type": "array"
},
"panelID": {
"description": "Numeric panel ID from dashboard JSON.\nUsed to correlate with dashboard structure.",
"type": "integer"
},
"panelTitle": {
"description": "Human-readable panel title for context.\nExample: \"CPU Usage\", \"Request Rate\"",
"type": "string"
},
"queryRefId": {
"description": "Query identifier within the panel.\nValues: \"A\", \"B\", \"C\", etc. (from panel.targets[].refId)\nUniquely identifies which query in a multi-query panel this refers to.",
"type": "string"
},
"totalMetrics": {
"description": "Number of unique metrics referenced in this specific query.\nFor Prometheus: metrics extracted from the PromQL expr.\nExample: rate(http_requests_total[5m]) references 1 metric.",
"type": "integer"
}
},
"required": [
"panelTitle",
"panelID",
"queryRefId",
"totalMetrics",
"foundMetrics",
"missingMetrics",
"compatibilityScore"
],
"type": "object"
},
"type": "array"
},
"totalMetrics": {
"description": "Total number of unique metrics/identifiers referenced across all queries.\nFor Prometheus: metric names extracted from PromQL expressions.\nFor SQL datasources: table and column names.",
"type": "integer"
},
"totalQueries": {
"description": "Total number of queries in the dashboard targeting this datasource.\nIncludes all panel targets/queries that reference this datasource.",
"type": "integer"
},
"type": {
"description": "Datasource type (matches DataSourceMapping.type)",
"type": "string"
},
"uid": {
"description": "Datasource UID that was validated (matches DataSourceMapping.uid)",
"type": "string"
}
},
"required": [
"uid",
"type",
"totalQueries",
"checkedQueries",
"totalMetrics",
"foundMetrics",
"missingMetrics",
"queryBreakdown",
"compatibilityScore"
],
"type": "object"
},
"type": "array"
},
"lastChecked": {
"description": "ISO 8601 timestamp of when validation was last performed.\nExample: \"2024-01-15T10:30:00Z\"",
"type": "string"
},
"message": {
"description": "Human-readable summary of validation result.\nExamples: \"All queries compatible\", \"3 missing metrics found\"",
"type": "string"
},
"operatorStates": {
"additionalProperties": {
"properties": {
"descriptiveState": {
"description": "descriptiveState is an optional more descriptive state field which has no requirements on format",
"type": "string"
},
"details": {
"description": "details contains any extra information that is operator-specific",
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
},
"lastEvaluation": {
"description": "lastEvaluation is the ResourceVersion last evaluated",
"type": "string"
},
"state": {
"description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.",
"enum": ["success", "in_progress", "failed"],
"type": "string"
}
},
"required": ["lastEvaluation", "state"],
"type": "object"
},
"description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.",
"type": "object"
}
},
"required": ["compatibilityScore", "datasourceResults"],
"type": "object"
}
},
"required": ["spec"],
"type": "object"
}
},
"subresources": {
"status": {}
}
}
],
"names": {
"kind": "DashboardCompatibilityScore",
"plural": "dashboardcompatibilityscores"
},
"scope": "Namespaced"
}
}

View File

@@ -0,0 +1,223 @@
{
"apiVersion": "apps.grafana.com/v1alpha1",
"kind": "AppManifest",
"metadata": {
"name": "dashvalidator"
},
"spec": {
"appName": "dashvalidator",
"group": "dashvalidator.ext.grafana.com",
"versions": [
{
"name": "v1alpha1",
"served": true,
"kinds": [
{
"kind": "DashboardCompatibilityScore",
"plural": "DashboardCompatibilityScores",
"scope": "Namespaced",
"schema": {
"spec": {
"properties": {
"dashboardJson": {
"description": "Complete dashboard JSON object to validate.\nMust be a v1 dashboard schema (contains \"panels\" array).\nv2 dashboards (with \"elements\" structure) are not yet supported.",
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
},
"datasourceMappings": {
"description": "Array of datasources to validate against.\nThe validator will check dashboard queries against each datasource\nand provide per-datasource compatibility results.\n\nMVP: Only single datasource supported (array length = 1), Prometheus type only.\nFuture: Will support multiple datasources for dashboards with mixed queries.",
"items": {
"description": "DataSourceMapping specifies a datasource to validate dashboard queries against.\nMaps logical datasource references in the dashboard to actual datasource instances.",
"properties": {
"name": {
"description": "Optional human-readable name for display in results.\nIf not provided, UID will be used in error messages.\nExample: \"Production Prometheus (US-West)\"",
"type": "string"
},
"type": {
"description": "Type of datasource plugin.\nMVP: Only \"prometheus\" supported.\nFuture: \"mysql\", \"postgres\", \"elasticsearch\", etc.",
"type": "string"
},
"uid": {
"description": "Unique identifier of the datasource instance.\nExample: \"prometheus-prod-us-west\"",
"type": "string"
}
},
"required": ["uid", "type"],
"type": "object"
},
"type": "array"
}
},
"required": ["dashboardJson", "datasourceMappings"],
"type": "object"
},
"status": {
"properties": {
"additionalFields": {
"description": "additionalFields is reserved for future use",
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
},
"compatibilityScore": {
"description": "Overall compatibility score across all datasources (0-100).\nCalculated as: (total found metrics / total referenced metrics) * 100\n\nScore interpretation:\n- 100: Perfect compatibility, all queries will work\n- 80-99: Excellent, minor missing metrics\n- 50-79: Fair, significant missing metrics\n- 0-49: Poor, most queries will fail",
"type": "number"
},
"datasourceResults": {
"description": "Per-datasource validation results.\nArray length matches spec.datasourceMappings.\nEach element contains detailed metrics and query-level breakdown.",
"items": {
"description": "DataSourceResult contains validation results for a single datasource.\nProvides aggregate statistics and per-query breakdown of compatibility.",
"properties": {
"checkedQueries": {
"description": "Number of queries successfully validated.\nMay be less than totalQueries if some queries couldn't be parsed.",
"type": "integer"
},
"compatibilityScore": {
"description": "Overall compatibility score for this datasource (0-100).\nCalculated as: (foundMetrics / totalMetrics) * 100\nUsed to calculate the global compatibilityScore in status.",
"type": "number"
},
"foundMetrics": {
"description": "Number of metrics that exist in the datasource schema.\nfoundMetrics \u003c= totalMetrics",
"type": "integer"
},
"missingMetrics": {
"description": "Array of metric names that were referenced but don't exist.\nUseful for debugging why a dashboard shows \"no data\".\nExample for Prometheus: [\"http_requests_total\", \"api_latency_seconds\"]",
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"description": "Optional display name (matches DataSourceMapping.name if provided)",
"type": "string"
},
"queryBreakdown": {
"description": "Per-query breakdown showing which specific queries have issues.\nOne entry per query target (refId: \"A\", \"B\", \"C\", etc.) in each panel.\nAllows pinpointing exactly which panel/query needs fixing.",
"items": {
"description": "QueryBreakdown provides compatibility details for a single query within a panel.\nGranular per-query results allow users to identify exactly which queries need fixing.\n\nNote: A panel can have multiple queries (refId: \"A\", \"B\", \"C\", etc.),\nso there may be multiple QueryBreakdown entries for the same panelID.",
"properties": {
"compatibilityScore": {
"description": "Compatibility percentage for this individual query (0-100).\nCalculated as: (foundMetrics / totalMetrics) * 100\n100 = query will work perfectly, 0 = query will return no data.",
"type": "number"
},
"foundMetrics": {
"description": "Number of those metrics that exist in the datasource.\nfoundMetrics \u003c= totalMetrics",
"type": "integer"
},
"missingMetrics": {
"description": "Array of missing metric names specific to this query.\nHelps identify exactly which part of a query expression will fail.\nEmpty array means query is fully compatible.",
"items": {
"type": "string"
},
"type": "array"
},
"panelID": {
"description": "Numeric panel ID from dashboard JSON.\nUsed to correlate with dashboard structure.",
"type": "integer"
},
"panelTitle": {
"description": "Human-readable panel title for context.\nExample: \"CPU Usage\", \"Request Rate\"",
"type": "string"
},
"queryRefId": {
"description": "Query identifier within the panel.\nValues: \"A\", \"B\", \"C\", etc. (from panel.targets[].refId)\nUniquely identifies which query in a multi-query panel this refers to.",
"type": "string"
},
"totalMetrics": {
"description": "Number of unique metrics referenced in this specific query.\nFor Prometheus: metrics extracted from the PromQL expr.\nExample: rate(http_requests_total[5m]) references 1 metric.",
"type": "integer"
}
},
"required": [
"panelTitle",
"panelID",
"queryRefId",
"totalMetrics",
"foundMetrics",
"missingMetrics",
"compatibilityScore"
],
"type": "object"
},
"type": "array"
},
"totalMetrics": {
"description": "Total number of unique metrics/identifiers referenced across all queries.\nFor Prometheus: metric names extracted from PromQL expressions.\nFor SQL datasources: table and column names.",
"type": "integer"
},
"totalQueries": {
"description": "Total number of queries in the dashboard targeting this datasource.\nIncludes all panel targets/queries that reference this datasource.",
"type": "integer"
},
"type": {
"description": "Datasource type (matches DataSourceMapping.type)",
"type": "string"
},
"uid": {
"description": "Datasource UID that was validated (matches DataSourceMapping.uid)",
"type": "string"
}
},
"required": [
"uid",
"type",
"totalQueries",
"checkedQueries",
"totalMetrics",
"foundMetrics",
"missingMetrics",
"queryBreakdown",
"compatibilityScore"
],
"type": "object"
},
"type": "array"
},
"lastChecked": {
"description": "ISO 8601 timestamp of when validation was last performed.\nExample: \"2024-01-15T10:30:00Z\"",
"type": "string"
},
"message": {
"description": "Human-readable summary of validation result.\nExamples: \"All queries compatible\", \"3 missing metrics found\"",
"type": "string"
},
"operatorStates": {
"additionalProperties": {
"properties": {
"descriptiveState": {
"description": "descriptiveState is an optional more descriptive state field which has no requirements on format",
"type": "string"
},
"details": {
"description": "details contains any extra information that is operator-specific",
"type": "object",
"x-kubernetes-preserve-unknown-fields": true
},
"lastEvaluation": {
"description": "lastEvaluation is the ResourceVersion last evaluated",
"type": "string"
},
"state": {
"description": "state describes the state of the lastEvaluation.\nIt is limited to three possible states for machine evaluation.",
"enum": ["success", "in_progress", "failed"],
"type": "string"
}
},
"required": ["lastEvaluation", "state"],
"type": "object"
},
"description": "operatorStates is a map of operator ID to operator state evaluations.\nAny operator which consumes this kind SHOULD add its state evaluation information to this field.",
"type": "object"
}
},
"required": ["compatibilityScore", "datasourceResults"],
"type": "object"
}
},
"conversion": false
}
]
}
],
"preferredVersion": "v1alpha1"
}
}

247
apps/dashvalidator/go.mod Normal file
View File

@@ -0,0 +1,247 @@
module github.com/grafana/grafana/apps/dashvalidator
go 1.25.5
require (
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
github.com/grafana/grafana-app-sdk v0.48.7
github.com/grafana/grafana-app-sdk/logging v0.48.7
github.com/prometheus/prometheus v0.303.1
k8s.io/apimachinery v0.34.3
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Machiel/slugify v1.0.1 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
github.com/aws/smithy-go v1.23.1 // indirect
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect
github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
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-migrate/migrate/v4 v4.7.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // indirect
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
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.284.0 // indirect
github.com/grafana/grafana/pkg/apimachinery v0.0.0 // indirect
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grafana/sqlds/v4 v4.2.7 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.7.0 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/memberlist v0.5.2 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/jaegertracing/jaeger-idl v0.5.0 // indirect
github.com/jmespath-community/go-jmespath v1.1.1 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/vsock v1.2.1 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mithrandie/csvq v1.18.1 // indirect
github.com/mithrandie/csvq-driver v1.7.0 // indirect
github.com/mithrandie/go-file/v2 v2.1.0 // indirect
github.com/mithrandie/go-text v1.6.0 // indirect
github.com/mithrandie/ternary v1.1.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nikunjy/rules v1.5.0 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/open-feature/go-sdk v1.16.0 // indirect
github.com/open-feature/go-sdk-contrib/providers/go-feature-flag v0.2.6 // indirect
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.6 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/alertmanager v0.28.2 // 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.4 // indirect
github.com/prometheus/exporter-toolkit v0.14.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/thomaspoignant/go-feature-flag v1.42.0 // indirect
github.com/tjhop/slog-gokit v0.1.5 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.32.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.3 // indirect
k8s.io/apiextensions-apiserver v0.34.3 // indirect
k8s.io/apiserver v0.34.3 // indirect
k8s.io/client-go v0.34.3 // indirect
k8s.io/component-base v0.34.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.40.1 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
xorm.io/builder v0.3.13 // indirect
)
// transitive dependencies that need replaced
// TODO: stop depending on grafana core
replace github.com/grafana/grafana => ../..
replace github.com/grafana/grafana/pkg/apimachinery => ../../pkg/apimachinery
replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver
replace github.com/grafana/grafana/apps/dashboard => ../dashboard
replace github.com/grafana/grafana/apps/provisioning => ../provisioning
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604

1019
apps/dashvalidator/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
module: "github.com/grafana/grafana/apps/dashvalidator/kinds"
language: version: "v0.8.2"

View File

@@ -0,0 +1,157 @@
package kinds
// DashboardCompatibilityScore validates whether a dashboard's queries
// are compatible with the target datasource schema.
//
// This resource checks if metrics, tables, or other identifiers referenced
// in dashboard queries actually exist in the configured datasources,
// helping users identify dashboards that will show "no data" before deployment.
//
// MVP: Prometheus datasource only; architecture supports future datasource types.
dashboardcompatibilityscorev0alpha1: {
kind: "DashboardCompatibilityScore"
plural: "dashboardcompatibilityscores"
scope: "Namespaced"
schema: {
spec: {
// Complete dashboard JSON object to validate.
// Must be a v1 dashboard schema (contains "panels" array).
// v2 dashboards (with "elements" structure) are not yet supported.
dashboardJson: {...}
// Array of datasources to validate against.
// The validator will check dashboard queries against each datasource
// and provide per-datasource compatibility results.
//
// MVP: Only single datasource supported (array length = 1), Prometheus type only.
// Future: Will support multiple datasources for dashboards with mixed queries.
datasourceMappings: [...#DataSourceMapping]
}
status: {
// Overall compatibility score across all datasources (0-100).
// Calculated as: (total found metrics / total referenced metrics) * 100
//
// Score interpretation:
// - 100: Perfect compatibility, all queries will work
// - 80-99: Excellent, minor missing metrics
// - 50-79: Fair, significant missing metrics
// - 0-49: Poor, most queries will fail
compatibilityScore: float64
// Per-datasource validation results.
// Array length matches spec.datasourceMappings.
// Each element contains detailed metrics and query-level breakdown.
datasourceResults: [...#DataSourceResult]
// ISO 8601 timestamp of when validation was last performed.
// Example: "2024-01-15T10:30:00Z"
lastChecked?: string
// Human-readable summary of validation result.
// Examples: "All queries compatible", "3 missing metrics found"
message?: string
}
}
}
// DataSourceMapping specifies a datasource to validate dashboard queries against.
// Maps logical datasource references in the dashboard to actual datasource instances.
#DataSourceMapping: {
// Unique identifier of the datasource instance.
// Example: "prometheus-prod-us-west"
uid: string
// Type of datasource plugin.
// MVP: Only "prometheus" supported.
// Future: "mysql", "postgres", "elasticsearch", etc.
type: string
// Optional human-readable name for display in results.
// If not provided, UID will be used in error messages.
// Example: "Production Prometheus (US-West)"
name?: string
}
// DataSourceResult contains validation results for a single datasource.
// Provides aggregate statistics and per-query breakdown of compatibility.
#DataSourceResult: {
// Datasource UID that was validated (matches DataSourceMapping.uid)
uid: string
// Datasource type (matches DataSourceMapping.type)
type: string
// Optional display name (matches DataSourceMapping.name if provided)
name?: string
// Total number of queries in the dashboard targeting this datasource.
// Includes all panel targets/queries that reference this datasource.
totalQueries: int
// Number of queries successfully validated.
// May be less than totalQueries if some queries couldn't be parsed.
checkedQueries: int
// Total number of unique metrics/identifiers referenced across all queries.
// For Prometheus: metric names extracted from PromQL expressions.
// For SQL datasources: table and column names.
totalMetrics: int
// Number of metrics that exist in the datasource schema.
// foundMetrics <= totalMetrics
foundMetrics: int
// Array of metric names that were referenced but don't exist.
// Useful for debugging why a dashboard shows "no data".
// Example for Prometheus: ["http_requests_total", "api_latency_seconds"]
missingMetrics: [...string]
// Per-query breakdown showing which specific queries have issues.
// One entry per query target (refId: "A", "B", "C", etc.) in each panel.
// Allows pinpointing exactly which panel/query needs fixing.
queryBreakdown: [...#QueryBreakdown]
// Overall compatibility score for this datasource (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// Used to calculate the global compatibilityScore in status.
compatibilityScore: float64
}
// QueryBreakdown provides compatibility details for a single query within a panel.
// Granular per-query results allow users to identify exactly which queries need fixing.
//
// Note: A panel can have multiple queries (refId: "A", "B", "C", etc.),
// so there may be multiple QueryBreakdown entries for the same panelID.
#QueryBreakdown: {
// Human-readable panel title for context.
// Example: "CPU Usage", "Request Rate"
panelTitle: string
// Numeric panel ID from dashboard JSON.
// Used to correlate with dashboard structure.
panelID: int
// Query identifier within the panel.
// Values: "A", "B", "C", etc. (from panel.targets[].refId)
// Uniquely identifies which query in a multi-query panel this refers to.
queryRefId: string
// Number of unique metrics referenced in this specific query.
// For Prometheus: metrics extracted from the PromQL expr.
// Example: rate(http_requests_total[5m]) references 1 metric.
totalMetrics: int
// Number of those metrics that exist in the datasource.
// foundMetrics <= totalMetrics
foundMetrics: int
// Array of missing metric names specific to this query.
// Helps identify exactly which part of a query expression will fail.
// Empty array means query is fully compatible.
missingMetrics: [...string]
// Compatibility percentage for this individual query (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// 100 = query will work perfectly, 0 = query will return no data.
compatibilityScore: float64
}

View File

@@ -0,0 +1,110 @@
package kinds
manifest: {
// appName is the unique name of your app. It is used to reference the app from other config objects,
// and to generate the group used by your app in the app platform API.
appName: "dashvalidator"
// groupOverride can be used to specify a non-appName-based API group.
// By default, an app's API group is LOWER(REPLACE(appName, '-', '')).ext.grafana.com,
// but there are cases where this needs to be changed.
// Keep in mind that changing this after an app is deployed can cause problems with clients and/or kind data.
groupOverride: "dashvalidator.grafana.app"
// versions is a map of versions supported by your app. Version names should follow the format "v<integer>" or
// "v<integer>(alpha|beta)<integer>". Each version contains the kinds your app manages for that version.
// If your app needs access to kinds managed by another app, use permissions.accessKinds to allow your app access.
versions: {
"v1alpha1": v1alpha1
}
// extraPermissions contains any additional permissions your app may require to function.
// Your app will always have all permissions for each kind it manages (the items defined in 'kinds').
extraPermissions: {
// If your app needs access to additional kinds supplied by other apps, you can list them here
accessKinds: [
// Here is an example for your app accessing the playlist kind for reads and watch
// {
// group: "playlist.grafana.app"
// resource: "playlists"
// actions: ["get","list","watch"]
// }
]
}
}
// v1alpha1 is the v1alpha1 version of the app's API.
// It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version.
v1alpha1: {
// kinds is the list of kinds served by this version
kinds: [dashboardcompatibilityscorev0alpha1]
// [OPTIONAL]
// served indicates whether this particular version is served by the API server.
// served should be set to false before a version is removed from the manifest entirely.
// served defaults to true if not present.
served: true
// [OPTIONAL]
// Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind.
// If not present, default values within the codegen trait are used.
// If you wish to specify codegen per-version, put this section in the version's object
// (for example, <no value>v1alpha1) instead.
routes: {
namespaced: {
"/check": {
"POST": {
request: {
body: {
dashboardJson: {...}
datasourceMappings: [...{
uid: string
type: string
name?: string
}]
}
}
response: {
compatibilityScore: number
datasourceResults: [...{
uid: string
type: string
name?: string
totalQueries: int
checkedQueries: int
totalMetrics: int
foundMetrics: int
missingMetrics: [...string]
queryBreakdown: [...{
panelTitle: string
panelID: int
queryRefId: string
totalMetrics: int
foundMetrics: int
missingMetrics: [...string]
compatibilityScore: number
}]
compatibilityScore: number
}]
}
}
}
}
cluser: {}
}
codegen: {
// [OPTIONAL]
// ts contains TypeScript code generation properties for the kind
ts: {
// [OPTIONAL]
// enabled indicates whether the CLI should generate front-end TypeScript code for the kind.
// Defaults to true if not present.
enabled: true
}
// [OPTIONAL]
// go contains go code generation properties for the kind
go: {
// [OPTIONAL]
// enabled indicates whether the CLI should generate back-end go code for the kind.
// Defaults to true if not present.
enabled: true
}
}
}

View File

@@ -4,7 +4,7 @@ import "k8s.io/apimachinery/pkg/runtime/schema"
const (
// APIGroup is the API group used by all kinds in this package
APIGroup = "logsdrilldown.grafana.app"
APIGroup = "dashvalidator.grafana.app"
// APIVersion is the API version used by all kinds in this package
APIVersion = "v1alpha1"
)

View File

@@ -0,0 +1,27 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
type CreateCheckRequestBody struct {
DashboardJson map[string]any `json:"dashboardJson"`
DatasourceMappings []CreateCheckRequestV1alpha1BodyDatasourceMappings `json:"datasourceMappings"`
}
// NewCreateCheckRequestBody creates a new CreateCheckRequestBody object.
func NewCreateCheckRequestBody() *CreateCheckRequestBody {
return &CreateCheckRequestBody{
DashboardJson: map[string]any{},
DatasourceMappings: []CreateCheckRequestV1alpha1BodyDatasourceMappings{},
}
}
type CreateCheckRequestV1alpha1BodyDatasourceMappings struct {
Uid string `json:"uid"`
Type string `json:"type"`
Name *string `json:"name,omitempty"`
}
// NewCreateCheckRequestV1alpha1BodyDatasourceMappings creates a new CreateCheckRequestV1alpha1BodyDatasourceMappings object.
func NewCreateCheckRequestV1alpha1BodyDatasourceMappings() *CreateCheckRequestV1alpha1BodyDatasourceMappings {
return &CreateCheckRequestV1alpha1BodyDatasourceMappings{}
}

View File

@@ -0,0 +1,56 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
// +k8s:openapi-gen=true
type CreateCheckBody struct {
CompatibilityScore float64 `json:"compatibilityScore"`
DatasourceResults []V1alpha1CreateCheckBodyDatasourceResults `json:"datasourceResults"`
}
// NewCreateCheckBody creates a new CreateCheckBody object.
func NewCreateCheckBody() *CreateCheckBody {
return &CreateCheckBody{
DatasourceResults: []V1alpha1CreateCheckBodyDatasourceResults{},
}
}
// +k8s:openapi-gen=true
type V1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown struct {
PanelTitle string `json:"panelTitle"`
PanelID int64 `json:"panelID"`
QueryRefId string `json:"queryRefId"`
TotalMetrics int64 `json:"totalMetrics"`
FoundMetrics int64 `json:"foundMetrics"`
MissingMetrics []string `json:"missingMetrics"`
CompatibilityScore float64 `json:"compatibilityScore"`
}
// NewV1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown creates a new V1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown object.
func NewV1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown() *V1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown {
return &V1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown{
MissingMetrics: []string{},
}
}
// +k8s:openapi-gen=true
type V1alpha1CreateCheckBodyDatasourceResults struct {
Uid string `json:"uid"`
Type string `json:"type"`
Name *string `json:"name,omitempty"`
TotalQueries int64 `json:"totalQueries"`
CheckedQueries int64 `json:"checkedQueries"`
TotalMetrics int64 `json:"totalMetrics"`
FoundMetrics int64 `json:"foundMetrics"`
MissingMetrics []string `json:"missingMetrics"`
QueryBreakdown []V1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown `json:"queryBreakdown"`
CompatibilityScore float64 `json:"compatibilityScore"`
}
// NewV1alpha1CreateCheckBodyDatasourceResults creates a new V1alpha1CreateCheckBodyDatasourceResults object.
func NewV1alpha1CreateCheckBodyDatasourceResults() *V1alpha1CreateCheckBodyDatasourceResults {
return &V1alpha1CreateCheckBodyDatasourceResults{
MissingMetrics: []string{},
QueryBreakdown: []V1alpha1CreateCheckBodyDatasourceResultsQueryBreakdown{},
}
}

View File

@@ -0,0 +1,37 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// +k8s:openapi-gen=true
type CreateCheck struct {
metav1.TypeMeta `json:",inline"`
CreateCheckBody `json:",inline"`
}
func NewCreateCheck() *CreateCheck {
return &CreateCheck{}
}
func (t *CreateCheckBody) DeepCopyInto(dst *CreateCheckBody) {
_ = resource.CopyObjectInto(dst, t)
}
func (o *CreateCheck) DeepCopyObject() runtime.Object {
dst := NewCreateCheck()
o.DeepCopyInto(dst)
return dst
}
func (o *CreateCheck) DeepCopyInto(dst *CreateCheck) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.CreateCheckBody.DeepCopyInto(&dst.CreateCheckBody)
}
var _ runtime.Object = NewCreateCheck()

View File

@@ -0,0 +1,99 @@
package v1alpha1
import (
"context"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type DashboardCompatibilityScoreClient struct {
client *resource.TypedClient[*DashboardCompatibilityScore, *DashboardCompatibilityScoreList]
}
func NewDashboardCompatibilityScoreClient(client resource.Client) *DashboardCompatibilityScoreClient {
return &DashboardCompatibilityScoreClient{
client: resource.NewTypedClient[*DashboardCompatibilityScore, *DashboardCompatibilityScoreList](client, DashboardCompatibilityScoreKind()),
}
}
func NewDashboardCompatibilityScoreClientFromGenerator(generator resource.ClientGenerator) (*DashboardCompatibilityScoreClient, error) {
c, err := generator.ClientFor(DashboardCompatibilityScoreKind())
if err != nil {
return nil, err
}
return NewDashboardCompatibilityScoreClient(c), nil
}
func (c *DashboardCompatibilityScoreClient) Get(ctx context.Context, identifier resource.Identifier) (*DashboardCompatibilityScore, error) {
return c.client.Get(ctx, identifier)
}
func (c *DashboardCompatibilityScoreClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*DashboardCompatibilityScoreList, error) {
return c.client.List(ctx, namespace, opts)
}
func (c *DashboardCompatibilityScoreClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*DashboardCompatibilityScoreList, error) {
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
for resp.GetContinue() != "" {
page, err := c.client.List(ctx, namespace, resource.ListOptions{
Continue: resp.GetContinue(),
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
resp.SetContinue(page.GetContinue())
resp.SetResourceVersion(page.GetResourceVersion())
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
}
return resp, nil
}
func (c *DashboardCompatibilityScoreClient) Create(ctx context.Context, obj *DashboardCompatibilityScore, opts resource.CreateOptions) (*DashboardCompatibilityScore, error) {
// Make sure apiVersion and kind are set
obj.APIVersion = GroupVersion.Identifier()
obj.Kind = DashboardCompatibilityScoreKind().Kind()
return c.client.Create(ctx, obj, opts)
}
func (c *DashboardCompatibilityScoreClient) Update(ctx context.Context, obj *DashboardCompatibilityScore, opts resource.UpdateOptions) (*DashboardCompatibilityScore, error) {
return c.client.Update(ctx, obj, opts)
}
func (c *DashboardCompatibilityScoreClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*DashboardCompatibilityScore, error) {
return c.client.Patch(ctx, identifier, req, opts)
}
func (c *DashboardCompatibilityScoreClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus DashboardCompatibilityScoreStatus, opts resource.UpdateOptions) (*DashboardCompatibilityScore, error) {
return c.client.Update(ctx, &DashboardCompatibilityScore{
TypeMeta: metav1.TypeMeta{
Kind: DashboardCompatibilityScoreKind().Kind(),
APIVersion: GroupVersion.Identifier(),
},
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: opts.ResourceVersion,
Namespace: identifier.Namespace,
Name: identifier.Name,
},
Status: newStatus,
}, resource.UpdateOptions{
Subresource: "status",
ResourceVersion: opts.ResourceVersion,
})
}
func (c *DashboardCompatibilityScoreClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
return c.client.Delete(ctx, identifier, opts)
}

View File

@@ -11,18 +11,18 @@ import (
"github.com/grafana/grafana-app-sdk/resource"
)
// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type JSONCodec struct{}
// DashboardCompatibilityScoreJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type DashboardCompatibilityScoreJSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*JSONCodec) Read(reader io.Reader, into resource.Object) error {
func (*DashboardCompatibilityScoreJSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*JSONCodec) Write(writer io.Writer, from resource.Object) error {
func (*DashboardCompatibilityScoreJSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &JSONCodec{}
var _ resource.Codec = &DashboardCompatibilityScoreJSONCodec{}

View File

@@ -9,7 +9,7 @@ import (
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type Metadata struct {
type DashboardCompatibilityScoreMetadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
@@ -22,9 +22,9 @@ type Metadata struct {
Labels map[string]string `json:"labels"`
}
// NewMetadata creates a new Metadata object.
func NewMetadata() *Metadata {
return &Metadata{
// NewDashboardCompatibilityScoreMetadata creates a new DashboardCompatibilityScoreMetadata object.
func NewDashboardCompatibilityScoreMetadata() *DashboardCompatibilityScoreMetadata {
return &DashboardCompatibilityScoreMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}

View File

@@ -15,22 +15,29 @@ import (
)
// +k8s:openapi-gen=true
type LogsDrilldownDefaults struct {
type DashboardCompatibilityScore struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the LogsDrilldownDefaults
Spec Spec `json:"spec" yaml:"spec"`
// Spec is the spec of the DashboardCompatibilityScore
Spec DashboardCompatibilityScoreSpec `json:"spec" yaml:"spec"`
Status Status `json:"status" yaml:"status"`
Status DashboardCompatibilityScoreStatus `json:"status" yaml:"status"`
}
func (o *LogsDrilldownDefaults) GetSpec() any {
func NewDashboardCompatibilityScore() *DashboardCompatibilityScore {
return &DashboardCompatibilityScore{
Spec: *NewDashboardCompatibilityScoreSpec(),
Status: *NewDashboardCompatibilityScoreStatus(),
}
}
func (o *DashboardCompatibilityScore) GetSpec() any {
return o.Spec
}
func (o *LogsDrilldownDefaults) SetSpec(spec any) error {
cast, ok := spec.(Spec)
func (o *DashboardCompatibilityScore) SetSpec(spec any) error {
cast, ok := spec.(DashboardCompatibilityScoreSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
@@ -38,13 +45,13 @@ func (o *LogsDrilldownDefaults) SetSpec(spec any) error {
return nil
}
func (o *LogsDrilldownDefaults) GetSubresources() map[string]any {
func (o *DashboardCompatibilityScore) GetSubresources() map[string]any {
return map[string]any{
"status": o.Status,
}
}
func (o *LogsDrilldownDefaults) GetSubresource(name string) (any, bool) {
func (o *DashboardCompatibilityScore) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.Status, true
@@ -53,12 +60,12 @@ func (o *LogsDrilldownDefaults) GetSubresource(name string) (any, bool) {
}
}
func (o *LogsDrilldownDefaults) SetSubresource(name string, value any) error {
func (o *DashboardCompatibilityScore) SetSubresource(name string, value any) error {
switch name {
case "status":
cast, ok := value.(Status)
cast, ok := value.(DashboardCompatibilityScoreStatus)
if !ok {
return fmt.Errorf("cannot set status type %#v, not of type Status", value)
return fmt.Errorf("cannot set status type %#v, not of type DashboardCompatibilityScoreStatus", value)
}
o.Status = cast
return nil
@@ -67,7 +74,7 @@ func (o *LogsDrilldownDefaults) SetSubresource(name string, value any) error {
}
}
func (o *LogsDrilldownDefaults) GetStaticMetadata() resource.StaticMetadata {
func (o *DashboardCompatibilityScore) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
@@ -78,7 +85,7 @@ func (o *LogsDrilldownDefaults) GetStaticMetadata() resource.StaticMetadata {
}
}
func (o *LogsDrilldownDefaults) SetStaticMetadata(metadata resource.StaticMetadata) {
func (o *DashboardCompatibilityScore) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
@@ -88,7 +95,7 @@ func (o *LogsDrilldownDefaults) SetStaticMetadata(metadata resource.StaticMetada
})
}
func (o *LogsDrilldownDefaults) GetCommonMetadata() resource.CommonMetadata {
func (o *DashboardCompatibilityScore) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
@@ -120,7 +127,7 @@ func (o *LogsDrilldownDefaults) GetCommonMetadata() resource.CommonMetadata {
}
}
func (o *LogsDrilldownDefaults) SetCommonMetadata(metadata resource.CommonMetadata) {
func (o *DashboardCompatibilityScore) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
@@ -165,7 +172,7 @@ func (o *LogsDrilldownDefaults) SetCommonMetadata(metadata resource.CommonMetada
}
}
func (o *LogsDrilldownDefaults) GetCreatedBy() string {
func (o *DashboardCompatibilityScore) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -173,7 +180,7 @@ func (o *LogsDrilldownDefaults) GetCreatedBy() string {
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *LogsDrilldownDefaults) SetCreatedBy(createdBy string) {
func (o *DashboardCompatibilityScore) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -181,7 +188,7 @@ func (o *LogsDrilldownDefaults) SetCreatedBy(createdBy string) {
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *LogsDrilldownDefaults) GetUpdateTimestamp() time.Time {
func (o *DashboardCompatibilityScore) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -190,7 +197,7 @@ func (o *LogsDrilldownDefaults) GetUpdateTimestamp() time.Time {
return parsed
}
func (o *LogsDrilldownDefaults) SetUpdateTimestamp(updateTimestamp time.Time) {
func (o *DashboardCompatibilityScore) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -198,7 +205,7 @@ func (o *LogsDrilldownDefaults) SetUpdateTimestamp(updateTimestamp time.Time) {
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *LogsDrilldownDefaults) GetUpdatedBy() string {
func (o *DashboardCompatibilityScore) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -206,7 +213,7 @@ func (o *LogsDrilldownDefaults) GetUpdatedBy() string {
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *LogsDrilldownDefaults) SetUpdatedBy(updatedBy string) {
func (o *DashboardCompatibilityScore) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -214,21 +221,21 @@ func (o *LogsDrilldownDefaults) SetUpdatedBy(updatedBy string) {
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *LogsDrilldownDefaults) Copy() resource.Object {
func (o *DashboardCompatibilityScore) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *LogsDrilldownDefaults) DeepCopyObject() runtime.Object {
func (o *DashboardCompatibilityScore) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *LogsDrilldownDefaults) DeepCopy() *LogsDrilldownDefaults {
cpy := &LogsDrilldownDefaults{}
func (o *DashboardCompatibilityScore) DeepCopy() *DashboardCompatibilityScore {
cpy := &DashboardCompatibilityScore{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *LogsDrilldownDefaults) DeepCopyInto(dst *LogsDrilldownDefaults) {
func (o *DashboardCompatibilityScore) DeepCopyInto(dst *DashboardCompatibilityScore) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
@@ -237,34 +244,34 @@ func (o *LogsDrilldownDefaults) DeepCopyInto(dst *LogsDrilldownDefaults) {
}
// Interface compliance compile-time check
var _ resource.Object = &LogsDrilldownDefaults{}
var _ resource.Object = &DashboardCompatibilityScore{}
// +k8s:openapi-gen=true
type LogsDrilldownDefaultsList struct {
type DashboardCompatibilityScoreList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []LogsDrilldownDefaults `json:"items" yaml:"items"`
Items []DashboardCompatibilityScore `json:"items" yaml:"items"`
}
func (o *LogsDrilldownDefaultsList) DeepCopyObject() runtime.Object {
func (o *DashboardCompatibilityScoreList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *LogsDrilldownDefaultsList) Copy() resource.ListObject {
cpy := &LogsDrilldownDefaultsList{
func (o *DashboardCompatibilityScoreList) Copy() resource.ListObject {
cpy := &DashboardCompatibilityScoreList{
TypeMeta: o.TypeMeta,
Items: make([]LogsDrilldownDefaults, len(o.Items)),
Items: make([]DashboardCompatibilityScore, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaults); ok {
if item, ok := o.Items[i].Copy().(*DashboardCompatibilityScore); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *LogsDrilldownDefaultsList) GetItems() []resource.Object {
func (o *DashboardCompatibilityScoreList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
@@ -272,48 +279,48 @@ func (o *LogsDrilldownDefaultsList) GetItems() []resource.Object {
return items
}
func (o *LogsDrilldownDefaultsList) SetItems(items []resource.Object) {
o.Items = make([]LogsDrilldownDefaults, len(items))
func (o *DashboardCompatibilityScoreList) SetItems(items []resource.Object) {
o.Items = make([]DashboardCompatibilityScore, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*LogsDrilldownDefaults)
o.Items[i] = *items[i].(*DashboardCompatibilityScore)
}
}
func (o *LogsDrilldownDefaultsList) DeepCopy() *LogsDrilldownDefaultsList {
cpy := &LogsDrilldownDefaultsList{}
func (o *DashboardCompatibilityScoreList) DeepCopy() *DashboardCompatibilityScoreList {
cpy := &DashboardCompatibilityScoreList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *LogsDrilldownDefaultsList) DeepCopyInto(dst *LogsDrilldownDefaultsList) {
func (o *DashboardCompatibilityScoreList) DeepCopyInto(dst *DashboardCompatibilityScoreList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &LogsDrilldownDefaultsList{}
var _ resource.ListObject = &DashboardCompatibilityScoreList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *Spec) DeepCopy() *Spec {
cpy := &Spec{}
func (s *DashboardCompatibilityScoreSpec) DeepCopy() *DashboardCompatibilityScoreSpec {
cpy := &DashboardCompatibilityScoreSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *Spec) DeepCopyInto(dst *Spec) {
func (s *DashboardCompatibilityScoreSpec) DeepCopyInto(dst *DashboardCompatibilityScoreSpec) {
resource.CopyObjectInto(dst, s)
}
// DeepCopy creates a full deep copy of Status
func (s *Status) DeepCopy() *Status {
cpy := &Status{}
// DeepCopy creates a full deep copy of DashboardCompatibilityScoreStatus
func (s *DashboardCompatibilityScoreStatus) DeepCopy() *DashboardCompatibilityScoreStatus {
cpy := &DashboardCompatibilityScoreStatus{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Status into another Status object
func (s *Status) DeepCopyInto(dst *Status) {
// DeepCopyInto deep copies DashboardCompatibilityScoreStatus into another DashboardCompatibilityScoreStatus object
func (s *DashboardCompatibilityScoreStatus) DeepCopyInto(dst *DashboardCompatibilityScoreStatus) {
resource.CopyObjectInto(dst, s)
}

View File

@@ -0,0 +1,34 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaDashboardCompatibilityScore = resource.NewSimpleSchema("dashvalidator.grafana.app", "v1alpha1", NewDashboardCompatibilityScore(), &DashboardCompatibilityScoreList{}, resource.WithKind("DashboardCompatibilityScore"),
resource.WithPlural("dashboardcompatibilityscores"), resource.WithScope(resource.NamespacedScope))
kindDashboardCompatibilityScore = resource.Kind{
Schema: schemaDashboardCompatibilityScore,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &DashboardCompatibilityScoreJSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func DashboardCompatibilityScoreKind() resource.Kind {
return kindDashboardCompatibilityScore
}
// Schema returns a resource.SimpleSchema representation of DashboardCompatibilityScore
func DashboardCompatibilityScoreSchema() *resource.SimpleSchema {
return schemaDashboardCompatibilityScore
}
// Interface compliance checks
var _ resource.Schema = kindDashboardCompatibilityScore

View File

@@ -0,0 +1,48 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
// DataSourceMapping specifies a datasource to validate dashboard queries against.
// Maps logical datasource references in the dashboard to actual datasource instances.
// +k8s:openapi-gen=true
type DashboardCompatibilityScoreDataSourceMapping struct {
// Unique identifier of the datasource instance.
// Example: "prometheus-prod-us-west"
Uid string `json:"uid"`
// Type of datasource plugin.
// MVP: Only "prometheus" supported.
// Future: "mysql", "postgres", "elasticsearch", etc.
Type string `json:"type"`
// Optional human-readable name for display in results.
// If not provided, UID will be used in error messages.
// Example: "Production Prometheus (US-West)"
Name *string `json:"name,omitempty"`
}
// NewDashboardCompatibilityScoreDataSourceMapping creates a new DashboardCompatibilityScoreDataSourceMapping object.
func NewDashboardCompatibilityScoreDataSourceMapping() *DashboardCompatibilityScoreDataSourceMapping {
return &DashboardCompatibilityScoreDataSourceMapping{}
}
// +k8s:openapi-gen=true
type DashboardCompatibilityScoreSpec struct {
// Complete dashboard JSON object to validate.
// Must be a v1 dashboard schema (contains "panels" array).
// v2 dashboards (with "elements" structure) are not yet supported.
DashboardJson map[string]interface{} `json:"dashboardJson"`
// Array of datasources to validate against.
// The validator will check dashboard queries against each datasource
// and provide per-datasource compatibility results.
//
// MVP: Only single datasource supported (array length = 1), Prometheus type only.
// Future: Will support multiple datasources for dashboards with mixed queries.
DatasourceMappings []DashboardCompatibilityScoreDataSourceMapping `json:"datasourceMappings"`
}
// NewDashboardCompatibilityScoreSpec creates a new DashboardCompatibilityScoreSpec object.
func NewDashboardCompatibilityScoreSpec() *DashboardCompatibilityScoreSpec {
return &DashboardCompatibilityScoreSpec{
DashboardJson: map[string]interface{}{},
DatasourceMappings: []DashboardCompatibilityScoreDataSourceMapping{},
}
}

View File

@@ -0,0 +1,151 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
// DataSourceResult contains validation results for a single datasource.
// Provides aggregate statistics and per-query breakdown of compatibility.
// +k8s:openapi-gen=true
type DashboardCompatibilityScoreDataSourceResult struct {
// Datasource UID that was validated (matches DataSourceMapping.uid)
Uid string `json:"uid"`
// Datasource type (matches DataSourceMapping.type)
Type string `json:"type"`
// Optional display name (matches DataSourceMapping.name if provided)
Name *string `json:"name,omitempty"`
// Total number of queries in the dashboard targeting this datasource.
// Includes all panel targets/queries that reference this datasource.
TotalQueries int64 `json:"totalQueries"`
// Number of queries successfully validated.
// May be less than totalQueries if some queries couldn't be parsed.
CheckedQueries int64 `json:"checkedQueries"`
// Total number of unique metrics/identifiers referenced across all queries.
// For Prometheus: metric names extracted from PromQL expressions.
// For SQL datasources: table and column names.
TotalMetrics int64 `json:"totalMetrics"`
// Number of metrics that exist in the datasource schema.
// foundMetrics <= totalMetrics
FoundMetrics int64 `json:"foundMetrics"`
// Array of metric names that were referenced but don't exist.
// Useful for debugging why a dashboard shows "no data".
// Example for Prometheus: ["http_requests_total", "api_latency_seconds"]
MissingMetrics []string `json:"missingMetrics"`
// Per-query breakdown showing which specific queries have issues.
// One entry per query target (refId: "A", "B", "C", etc.) in each panel.
// Allows pinpointing exactly which panel/query needs fixing.
QueryBreakdown []DashboardCompatibilityScoreQueryBreakdown `json:"queryBreakdown"`
// Overall compatibility score for this datasource (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// Used to calculate the global compatibilityScore in status.
CompatibilityScore float64 `json:"compatibilityScore"`
}
// NewDashboardCompatibilityScoreDataSourceResult creates a new DashboardCompatibilityScoreDataSourceResult object.
func NewDashboardCompatibilityScoreDataSourceResult() *DashboardCompatibilityScoreDataSourceResult {
return &DashboardCompatibilityScoreDataSourceResult{
MissingMetrics: []string{},
QueryBreakdown: []DashboardCompatibilityScoreQueryBreakdown{},
}
}
// QueryBreakdown provides compatibility details for a single query within a panel.
// Granular per-query results allow users to identify exactly which queries need fixing.
//
// Note: A panel can have multiple queries (refId: "A", "B", "C", etc.),
// so there may be multiple QueryBreakdown entries for the same panelID.
// +k8s:openapi-gen=true
type DashboardCompatibilityScoreQueryBreakdown struct {
// Human-readable panel title for context.
// Example: "CPU Usage", "Request Rate"
PanelTitle string `json:"panelTitle"`
// Numeric panel ID from dashboard JSON.
// Used to correlate with dashboard structure.
PanelID int64 `json:"panelID"`
// Query identifier within the panel.
// Values: "A", "B", "C", etc. (from panel.targets[].refId)
// Uniquely identifies which query in a multi-query panel this refers to.
QueryRefId string `json:"queryRefId"`
// Number of unique metrics referenced in this specific query.
// For Prometheus: metrics extracted from the PromQL expr.
// Example: rate(http_requests_total[5m]) references 1 metric.
TotalMetrics int64 `json:"totalMetrics"`
// Number of those metrics that exist in the datasource.
// foundMetrics <= totalMetrics
FoundMetrics int64 `json:"foundMetrics"`
// Array of missing metric names specific to this query.
// Helps identify exactly which part of a query expression will fail.
// Empty array means query is fully compatible.
MissingMetrics []string `json:"missingMetrics"`
// Compatibility percentage for this individual query (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// 100 = query will work perfectly, 0 = query will return no data.
CompatibilityScore float64 `json:"compatibilityScore"`
}
// NewDashboardCompatibilityScoreQueryBreakdown creates a new DashboardCompatibilityScoreQueryBreakdown object.
func NewDashboardCompatibilityScoreQueryBreakdown() *DashboardCompatibilityScoreQueryBreakdown {
return &DashboardCompatibilityScoreQueryBreakdown{
MissingMetrics: []string{},
}
}
// +k8s:openapi-gen=true
type DashboardCompatibilityScorestatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State DashboardCompatibilityScoreStatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewDashboardCompatibilityScorestatusOperatorState creates a new DashboardCompatibilityScorestatusOperatorState object.
func NewDashboardCompatibilityScorestatusOperatorState() *DashboardCompatibilityScorestatusOperatorState {
return &DashboardCompatibilityScorestatusOperatorState{}
}
// +k8s:openapi-gen=true
type DashboardCompatibilityScoreStatus struct {
// Overall compatibility score across all datasources (0-100).
// Calculated as: (total found metrics / total referenced metrics) * 100
//
// Score interpretation:
// - 100: Perfect compatibility, all queries will work
// - 80-99: Excellent, minor missing metrics
// - 50-79: Fair, significant missing metrics
// - 0-49: Poor, most queries will fail
CompatibilityScore float64 `json:"compatibilityScore"`
// Per-datasource validation results.
// Array length matches spec.datasourceMappings.
// Each element contains detailed metrics and query-level breakdown.
DatasourceResults []DashboardCompatibilityScoreDataSourceResult `json:"datasourceResults"`
// ISO 8601 timestamp of when validation was last performed.
// Example: "2024-01-15T10:30:00Z"
LastChecked *string `json:"lastChecked,omitempty"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]DashboardCompatibilityScorestatusOperatorState `json:"operatorStates,omitempty"`
// Human-readable summary of validation result.
// Examples: "All queries compatible", "3 missing metrics found"
Message *string `json:"message,omitempty"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewDashboardCompatibilityScoreStatus creates a new DashboardCompatibilityScoreStatus object.
func NewDashboardCompatibilityScoreStatus() *DashboardCompatibilityScoreStatus {
return &DashboardCompatibilityScoreStatus{
DatasourceResults: []DashboardCompatibilityScoreDataSourceResult{},
}
}
// +k8s:openapi-gen=true
type DashboardCompatibilityScoreStatusOperatorStateState string
const (
DashboardCompatibilityScoreStatusOperatorStateStateSuccess DashboardCompatibilityScoreStatusOperatorStateState = "success"
DashboardCompatibilityScoreStatusOperatorStateStateInProgress DashboardCompatibilityScoreStatusOperatorStateState = "in_progress"
DashboardCompatibilityScoreStatusOperatorStateStateFailed DashboardCompatibilityScoreStatusOperatorStateState = "failed"
)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,360 @@
package app
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/resource"
"github.com/grafana/grafana-app-sdk/simple"
"k8s.io/apimachinery/pkg/runtime/schema"
validatorv1alpha1 "github.com/grafana/grafana/apps/dashvalidator/pkg/apis/dashvalidator/v1alpha1"
"github.com/grafana/grafana/apps/dashvalidator/pkg/validator"
_ "github.com/grafana/grafana/apps/dashvalidator/pkg/validator/prometheus" // Register prometheus validator via init()
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"strings"
)
type DashValidatorConfig struct {
DatasourceSvc datasources.DataSourceService
PluginCtx *plugincontext.Provider
HTTPClientProvider httpclient.Provider
}
// checkRequest matches the CUE schema for POST /check request
type checkRequest struct {
DashboardJSON map[string]interface{} `json:"dashboardJson"`
DatasourceMappings []datasourceMapping `json:"datasourceMappings"`
}
// datasourceMapping represents a datasource to validate against
type datasourceMapping struct {
UID string `json:"uid"`
Type string `json:"type"`
Name *string `json:"name,omitempty"`
}
// checkResponse matches the CUE schema for POST /check response
type checkResponse struct {
CompatibilityScore float64 `json:"compatibilityScore"`
DatasourceResults []datasourceResult `json:"datasourceResults"`
}
// datasourceResult contains validation results for a single datasource
type datasourceResult struct {
UID string `json:"uid"`
Type string `json:"type"`
Name *string `json:"name,omitempty"`
TotalQueries int `json:"totalQueries"`
CheckedQueries int `json:"checkedQueries"`
TotalMetrics int `json:"totalMetrics"`
FoundMetrics int `json:"foundMetrics"`
MissingMetrics []string `json:"missingMetrics"`
QueryBreakdown []queryResult `json:"queryBreakdown"`
CompatibilityScore float64 `json:"compatibilityScore"`
}
// queryResult contains validation results for a single query
type queryResult struct {
PanelTitle string `json:"panelTitle"`
PanelID int `json:"panelID"`
QueryRefID string `json:"queryRefId"`
TotalMetrics int `json:"totalMetrics"`
FoundMetrics int `json:"foundMetrics"`
MissingMetrics []string `json:"missingMetrics"`
CompatibilityScore float64 `json:"compatibilityScore"`
}
func New(cfg app.Config) (app.App, error) {
specificConfig, ok := cfg.SpecificConfig.(*DashValidatorConfig)
if !ok {
return nil, fmt.Errorf("invalid config type: expected DashValidatorConfig")
}
log := logging.DefaultLogger.With("app", "dashvalidator")
// configure our app
simpleConfig := simple.AppConfig{
Name: "dashvalidator",
KubeConfig: cfg.KubeConfig,
//Define our custom route
VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{
"v1alpha1": {
{
Namespaced: true,
Path: "check",
Method: "POST",
}: handleCheckRoute(log, specificConfig.DatasourceSvc, specificConfig.PluginCtx, specificConfig.HTTPClientProvider),
},
},
}
a, err := simple.NewApp(simpleConfig)
if err != nil {
return nil, fmt.Errorf("failed to create app: %w", err)
}
return a, nil
}
// custom route handler to check dashboard compatibility
func handleCheckRoute(
log logging.Logger,
datasourceSvc datasources.DataSourceService,
pluginCtx *plugincontext.Provider,
httpClientProvider httpclient.Provider,
) func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error {
return func(ctx context.Context, w app.CustomRouteResponseWriter, r *app.CustomRouteRequest) error {
logger := log.WithContext(ctx)
logger.Info("Received compatibility check request")
// Step 1: Parse request body
body, err := io.ReadAll(r.Body)
if err != nil {
logger.Error("Failed to read request body", "error", err)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": "failed to read request body",
})
}
var req checkRequest
if err := json.Unmarshal(body, &req); err != nil {
logger.Error("Failed to parse request JSON", "error", err)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": "invalid JSON in request body",
})
}
// MVP: Only support single datasource validation
if len(req.DatasourceMappings) != 1 {
logger.Error("MVP only supports single datasource validation", "numDatasources", len(req.DatasourceMappings))
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("MVP only supports single datasource validation, got %d datasources", len(req.DatasourceMappings)),
"code": "invalid_request",
})
}
// Step 2: Build validator request
validatorReq := validator.DashboardCompatibilityRequest{
DashboardJSON: req.DashboardJSON,
DatasourceMappings: make([]validator.DatasourceMapping, 0, len(req.DatasourceMappings)),
}
logger.Info("Processing request", "dashboardTitle", req.DashboardJSON["title"], "numMappings", len(req.DatasourceMappings))
// Get namespace from request (needed for datasource lookup)
// Namespace format is typically "org-{orgID}"
namespace := r.ResourceIdentifier.Namespace
// Extract orgID from namespace for logging context
orgID := extractOrgIDFromNamespace(namespace)
logger = logger.With("orgID", orgID, "namespace", namespace)
for _, dsMapping := range req.DatasourceMappings {
dsLogger := logger.With("datasourceUID", dsMapping.UID, "datasourceType", dsMapping.Type)
// Convert optional name pointer to string
name := ""
if dsMapping.Name != nil {
name = *dsMapping.Name
dsLogger = dsLogger.With("datasourceName", name)
}
// Fetch datasource from Grafana using app-platform method
// Parameters: namespace, name (UID), group (datasource type)
ds, err := datasourceSvc.GetDataSourceInNamespace(ctx, namespace, dsMapping.UID, dsMapping.Type)
if err != nil {
dsLogger.Error("Failed to get datasource from namespace", "error", err)
// Check if it's a not found error vs other errors
errMsg := err.Error()
statusCode := http.StatusInternalServerError
userMsg := fmt.Sprintf("failed to retrieve datasource: %s", dsMapping.UID)
if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "does not exist") {
statusCode = http.StatusNotFound
userMsg = fmt.Sprintf("datasource not found: %s (type: %s)", dsMapping.UID, dsMapping.Type)
dsLogger.Warn("Datasource not found in namespace")
}
w.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(map[string]string{
"error": userMsg,
"code": "datasource_error",
})
}
dsLogger.Info("Retrieved datasource", "url", ds.URL, "actualType", ds.Type)
// Validate that the datasource type matches the expected type
if ds.Type != dsMapping.Type {
dsLogger.Error("Datasource type mismatch",
"expectedType", dsMapping.Type,
"actualType", ds.Type)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource %s has type %s, expected %s", dsMapping.UID, ds.Type, dsMapping.Type),
"code": "datasource_wrong_type",
})
}
// Validate that this is a supported datasource type
// For MVP, we only support Prometheus
if !isSupportedDatasourceType(ds.Type) {
dsLogger.Error("Unsupported datasource type", "type", ds.Type)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource type '%s' is not supported (currently only 'prometheus' is supported)", ds.Type),
"code": "datasource_unsupported_type",
})
}
// Get authenticated HTTP transport for this datasource
transport, err := datasourceSvc.GetHTTPTransport(ctx, ds, httpClientProvider)
if err != nil {
dsLogger.Error("Failed to get HTTP transport for datasource", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("failed to configure authentication for datasource: %s", dsMapping.UID),
"code": "datasource_config_error",
})
}
// Create HTTP client with authenticated transport
httpClient := &http.Client{
Transport: transport,
}
validatorReq.DatasourceMappings = append(validatorReq.DatasourceMappings, validator.DatasourceMapping{
UID: dsMapping.UID,
Type: dsMapping.Type,
Name: name,
URL: ds.URL,
HTTPClient: httpClient, // Pass authenticated client
})
dsLogger.Debug("Datasource configured successfully for validation")
}
// Step 3: Validate dashboard compatibility
result, err := validator.ValidateDashboardCompatibility(ctx, validatorReq)
if err != nil {
logger.Error("Validation failed", "error", err)
// Check if it's a structured ValidationError with a specific status code
statusCode := http.StatusInternalServerError
errorCode := "validation_error"
errorMsg := fmt.Sprintf("validation failed: %v", err)
if validationErr := validator.GetValidationError(err); validationErr != nil {
statusCode = validationErr.StatusCode
errorCode = string(validationErr.Code)
errorMsg = validationErr.Message
// Log additional context from the error
for key, value := range validationErr.Details {
logger.Error("Validation error detail", key, value)
}
}
w.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(map[string]string{
"error": errorMsg,
"code": errorCode,
})
}
// Step 4: Convert result to response format
response := convertToCheckResponse(result)
// Step 5: Return response
w.WriteHeader(http.StatusOK)
return json.NewEncoder(w).Encode(response)
}
}
// convertToCheckResponse converts validator result to API response format
func convertToCheckResponse(result *validator.DashboardCompatibilityResult) checkResponse {
response := checkResponse{
CompatibilityScore: result.CompatibilityScore,
DatasourceResults: make([]datasourceResult, 0, len(result.DatasourceResults)),
}
for _, dsResult := range result.DatasourceResults {
// Convert name string to pointer
var name *string
if dsResult.Name != "" {
name = &dsResult.Name
}
// Convert query results
queryBreakdown := make([]queryResult, 0, len(dsResult.QueryBreakdown))
for _, qr := range dsResult.QueryBreakdown {
queryBreakdown = append(queryBreakdown, queryResult{
PanelTitle: qr.PanelTitle,
PanelID: qr.PanelID,
QueryRefID: qr.QueryRefID,
TotalMetrics: qr.TotalMetrics,
FoundMetrics: qr.FoundMetrics,
MissingMetrics: qr.MissingMetrics,
CompatibilityScore: qr.CompatibilityScore,
})
}
response.DatasourceResults = append(response.DatasourceResults, datasourceResult{
UID: dsResult.UID,
Type: dsResult.Type,
Name: name,
TotalQueries: dsResult.TotalQueries,
CheckedQueries: dsResult.CheckedQueries,
TotalMetrics: dsResult.TotalMetrics,
FoundMetrics: dsResult.FoundMetrics,
MissingMetrics: dsResult.MissingMetrics,
QueryBreakdown: queryBreakdown,
CompatibilityScore: dsResult.CompatibilityScore,
})
}
return response
}
// extractOrgIDFromNamespace extracts the org ID from a namespace string
// Namespace format is typically "org-{orgID}"
func extractOrgIDFromNamespace(namespace string) string {
parts := strings.Split(namespace, "-")
if len(parts) >= 2 && parts[0] == "org" {
return parts[1]
}
return "unknown"
}
// isSupportedDatasourceType checks if a datasource type is supported
// For MVP, we only support Prometheus
func isSupportedDatasourceType(dsType string) bool {
supportedTypes := map[string]bool{
"prometheus": true,
}
return supportedTypes[strings.ToLower(dsType)]
}
func GetKinds() map[schema.GroupVersion][]resource.Kind {
gv := schema.GroupVersion{
Group: "dashvalidator.grafana.com",
Version: "v1alpha1",
}
return map[schema.GroupVersion][]resource.Kind{
gv: {validatorv1alpha1.DashboardCompatibilityScoreKind()},
}
}

View File

@@ -4,7 +4,7 @@ import "k8s.io/apimachinery/pkg/runtime/schema"
const (
// APIGroup is the API group used by all kinds in this package
APIGroup = "logsdrilldown.grafana.app"
APIGroup = "dashvalidator.ext.grafana.com"
// APIVersion is the API version used by all kinds in this package
APIVersion = "v1alpha1"
)

View File

@@ -7,33 +7,33 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type LogsDrilldownDefaultColumnsClient struct {
client *resource.TypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList]
type DashboardCompatibilityScoreClient struct {
client *resource.TypedClient[*DashboardCompatibilityScore, *DashboardCompatibilityScoreList]
}
func NewLogsDrilldownDefaultColumnsClient(client resource.Client) *LogsDrilldownDefaultColumnsClient {
return &LogsDrilldownDefaultColumnsClient{
client: resource.NewTypedClient[*LogsDrilldownDefaultColumns, *LogsDrilldownDefaultColumnsList](client, Kind()),
func NewDashboardCompatibilityScoreClient(client resource.Client) *DashboardCompatibilityScoreClient {
return &DashboardCompatibilityScoreClient{
client: resource.NewTypedClient[*DashboardCompatibilityScore, *DashboardCompatibilityScoreList](client, Kind()),
}
}
func NewLogsDrilldownDefaultColumnsClientFromGenerator(generator resource.ClientGenerator) (*LogsDrilldownDefaultColumnsClient, error) {
func NewDashboardCompatibilityScoreClientFromGenerator(generator resource.ClientGenerator) (*DashboardCompatibilityScoreClient, error) {
c, err := generator.ClientFor(Kind())
if err != nil {
return nil, err
}
return NewLogsDrilldownDefaultColumnsClient(c), nil
return NewDashboardCompatibilityScoreClient(c), nil
}
func (c *LogsDrilldownDefaultColumnsClient) Get(ctx context.Context, identifier resource.Identifier) (*LogsDrilldownDefaultColumns, error) {
func (c *DashboardCompatibilityScoreClient) Get(ctx context.Context, identifier resource.Identifier) (*DashboardCompatibilityScore, error) {
return c.client.Get(ctx, identifier)
}
func (c *LogsDrilldownDefaultColumnsClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) {
func (c *DashboardCompatibilityScoreClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*DashboardCompatibilityScoreList, error) {
return c.client.List(ctx, namespace, opts)
}
func (c *LogsDrilldownDefaultColumnsClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*LogsDrilldownDefaultColumnsList, error) {
func (c *DashboardCompatibilityScoreClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*DashboardCompatibilityScoreList, error) {
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
@@ -61,23 +61,23 @@ func (c *LogsDrilldownDefaultColumnsClient) ListAll(ctx context.Context, namespa
return resp, nil
}
func (c *LogsDrilldownDefaultColumnsClient) Create(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.CreateOptions) (*LogsDrilldownDefaultColumns, error) {
func (c *DashboardCompatibilityScoreClient) Create(ctx context.Context, obj *DashboardCompatibilityScore, opts resource.CreateOptions) (*DashboardCompatibilityScore, error) {
// Make sure apiVersion and kind are set
obj.APIVersion = GroupVersion.Identifier()
obj.Kind = Kind().Kind()
return c.client.Create(ctx, obj, opts)
}
func (c *LogsDrilldownDefaultColumnsClient) Update(ctx context.Context, obj *LogsDrilldownDefaultColumns, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) {
func (c *DashboardCompatibilityScoreClient) Update(ctx context.Context, obj *DashboardCompatibilityScore, opts resource.UpdateOptions) (*DashboardCompatibilityScore, error) {
return c.client.Update(ctx, obj, opts)
}
func (c *LogsDrilldownDefaultColumnsClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*LogsDrilldownDefaultColumns, error) {
func (c *DashboardCompatibilityScoreClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*DashboardCompatibilityScore, error) {
return c.client.Patch(ctx, identifier, req, opts)
}
func (c *LogsDrilldownDefaultColumnsClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*LogsDrilldownDefaultColumns, error) {
return c.client.Update(ctx, &LogsDrilldownDefaultColumns{
func (c *DashboardCompatibilityScoreClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus Status, opts resource.UpdateOptions) (*DashboardCompatibilityScore, error) {
return c.client.Update(ctx, &DashboardCompatibilityScore{
TypeMeta: metav1.TypeMeta{
Kind: Kind().Kind(),
APIVersion: GroupVersion.Identifier(),
@@ -94,6 +94,6 @@ func (c *LogsDrilldownDefaultColumnsClient) UpdateStatus(ctx context.Context, id
})
}
func (c *LogsDrilldownDefaultColumnsClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
func (c *DashboardCompatibilityScoreClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
return c.client.Delete(ctx, identifier, opts)
}

View File

@@ -15,21 +15,28 @@ import (
)
// +k8s:openapi-gen=true
type LogsDrilldownDefaultColumns struct {
type DashboardCompatibilityScore struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the LogsDrilldownDefaultColumns
// Spec is the spec of the DashboardCompatibilityScore
Spec Spec `json:"spec" yaml:"spec"`
Status Status `json:"status" yaml:"status"`
}
func (o *LogsDrilldownDefaultColumns) GetSpec() any {
func NewDashboardCompatibilityScore() *DashboardCompatibilityScore {
return &DashboardCompatibilityScore{
Spec: *NewSpec(),
Status: *NewStatus(),
}
}
func (o *DashboardCompatibilityScore) GetSpec() any {
return o.Spec
}
func (o *LogsDrilldownDefaultColumns) SetSpec(spec any) error {
func (o *DashboardCompatibilityScore) SetSpec(spec any) error {
cast, ok := spec.(Spec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
@@ -38,13 +45,13 @@ func (o *LogsDrilldownDefaultColumns) SetSpec(spec any) error {
return nil
}
func (o *LogsDrilldownDefaultColumns) GetSubresources() map[string]any {
func (o *DashboardCompatibilityScore) GetSubresources() map[string]any {
return map[string]any{
"status": o.Status,
}
}
func (o *LogsDrilldownDefaultColumns) GetSubresource(name string) (any, bool) {
func (o *DashboardCompatibilityScore) GetSubresource(name string) (any, bool) {
switch name {
case "status":
return o.Status, true
@@ -53,7 +60,7 @@ func (o *LogsDrilldownDefaultColumns) GetSubresource(name string) (any, bool) {
}
}
func (o *LogsDrilldownDefaultColumns) SetSubresource(name string, value any) error {
func (o *DashboardCompatibilityScore) SetSubresource(name string, value any) error {
switch name {
case "status":
cast, ok := value.(Status)
@@ -67,7 +74,7 @@ func (o *LogsDrilldownDefaultColumns) SetSubresource(name string, value any) err
}
}
func (o *LogsDrilldownDefaultColumns) GetStaticMetadata() resource.StaticMetadata {
func (o *DashboardCompatibilityScore) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
@@ -78,7 +85,7 @@ func (o *LogsDrilldownDefaultColumns) GetStaticMetadata() resource.StaticMetadat
}
}
func (o *LogsDrilldownDefaultColumns) SetStaticMetadata(metadata resource.StaticMetadata) {
func (o *DashboardCompatibilityScore) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
@@ -88,7 +95,7 @@ func (o *LogsDrilldownDefaultColumns) SetStaticMetadata(metadata resource.Static
})
}
func (o *LogsDrilldownDefaultColumns) GetCommonMetadata() resource.CommonMetadata {
func (o *DashboardCompatibilityScore) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
@@ -120,7 +127,7 @@ func (o *LogsDrilldownDefaultColumns) GetCommonMetadata() resource.CommonMetadat
}
}
func (o *LogsDrilldownDefaultColumns) SetCommonMetadata(metadata resource.CommonMetadata) {
func (o *DashboardCompatibilityScore) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
@@ -165,7 +172,7 @@ func (o *LogsDrilldownDefaultColumns) SetCommonMetadata(metadata resource.Common
}
}
func (o *LogsDrilldownDefaultColumns) GetCreatedBy() string {
func (o *DashboardCompatibilityScore) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -173,7 +180,7 @@ func (o *LogsDrilldownDefaultColumns) GetCreatedBy() string {
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *LogsDrilldownDefaultColumns) SetCreatedBy(createdBy string) {
func (o *DashboardCompatibilityScore) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -181,7 +188,7 @@ func (o *LogsDrilldownDefaultColumns) SetCreatedBy(createdBy string) {
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *LogsDrilldownDefaultColumns) GetUpdateTimestamp() time.Time {
func (o *DashboardCompatibilityScore) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -190,7 +197,7 @@ func (o *LogsDrilldownDefaultColumns) GetUpdateTimestamp() time.Time {
return parsed
}
func (o *LogsDrilldownDefaultColumns) SetUpdateTimestamp(updateTimestamp time.Time) {
func (o *DashboardCompatibilityScore) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -198,7 +205,7 @@ func (o *LogsDrilldownDefaultColumns) SetUpdateTimestamp(updateTimestamp time.Ti
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *LogsDrilldownDefaultColumns) GetUpdatedBy() string {
func (o *DashboardCompatibilityScore) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -206,7 +213,7 @@ func (o *LogsDrilldownDefaultColumns) GetUpdatedBy() string {
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *LogsDrilldownDefaultColumns) SetUpdatedBy(updatedBy string) {
func (o *DashboardCompatibilityScore) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
@@ -214,21 +221,21 @@ func (o *LogsDrilldownDefaultColumns) SetUpdatedBy(updatedBy string) {
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *LogsDrilldownDefaultColumns) Copy() resource.Object {
func (o *DashboardCompatibilityScore) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *LogsDrilldownDefaultColumns) DeepCopyObject() runtime.Object {
func (o *DashboardCompatibilityScore) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *LogsDrilldownDefaultColumns) DeepCopy() *LogsDrilldownDefaultColumns {
cpy := &LogsDrilldownDefaultColumns{}
func (o *DashboardCompatibilityScore) DeepCopy() *DashboardCompatibilityScore {
cpy := &DashboardCompatibilityScore{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *LogsDrilldownDefaultColumns) DeepCopyInto(dst *LogsDrilldownDefaultColumns) {
func (o *DashboardCompatibilityScore) DeepCopyInto(dst *DashboardCompatibilityScore) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
@@ -237,34 +244,34 @@ func (o *LogsDrilldownDefaultColumns) DeepCopyInto(dst *LogsDrilldownDefaultColu
}
// Interface compliance compile-time check
var _ resource.Object = &LogsDrilldownDefaultColumns{}
var _ resource.Object = &DashboardCompatibilityScore{}
// +k8s:openapi-gen=true
type LogsDrilldownDefaultColumnsList struct {
type DashboardCompatibilityScoreList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []LogsDrilldownDefaultColumns `json:"items" yaml:"items"`
Items []DashboardCompatibilityScore `json:"items" yaml:"items"`
}
func (o *LogsDrilldownDefaultColumnsList) DeepCopyObject() runtime.Object {
func (o *DashboardCompatibilityScoreList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *LogsDrilldownDefaultColumnsList) Copy() resource.ListObject {
cpy := &LogsDrilldownDefaultColumnsList{
func (o *DashboardCompatibilityScoreList) Copy() resource.ListObject {
cpy := &DashboardCompatibilityScoreList{
TypeMeta: o.TypeMeta,
Items: make([]LogsDrilldownDefaultColumns, len(o.Items)),
Items: make([]DashboardCompatibilityScore, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*LogsDrilldownDefaultColumns); ok {
if item, ok := o.Items[i].Copy().(*DashboardCompatibilityScore); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *LogsDrilldownDefaultColumnsList) GetItems() []resource.Object {
func (o *DashboardCompatibilityScoreList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
@@ -272,25 +279,25 @@ func (o *LogsDrilldownDefaultColumnsList) GetItems() []resource.Object {
return items
}
func (o *LogsDrilldownDefaultColumnsList) SetItems(items []resource.Object) {
o.Items = make([]LogsDrilldownDefaultColumns, len(items))
func (o *DashboardCompatibilityScoreList) SetItems(items []resource.Object) {
o.Items = make([]DashboardCompatibilityScore, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*LogsDrilldownDefaultColumns)
o.Items[i] = *items[i].(*DashboardCompatibilityScore)
}
}
func (o *LogsDrilldownDefaultColumnsList) DeepCopy() *LogsDrilldownDefaultColumnsList {
cpy := &LogsDrilldownDefaultColumnsList{}
func (o *DashboardCompatibilityScoreList) DeepCopy() *DashboardCompatibilityScoreList {
cpy := &DashboardCompatibilityScoreList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *LogsDrilldownDefaultColumnsList) DeepCopyInto(dst *LogsDrilldownDefaultColumnsList) {
func (o *DashboardCompatibilityScoreList) DeepCopyInto(dst *DashboardCompatibilityScoreList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &LogsDrilldownDefaultColumnsList{}
var _ resource.ListObject = &DashboardCompatibilityScoreList{}
// Copy methods for all subresource types

View File

@@ -0,0 +1,34 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v1alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaDashboardCompatibilityScore = resource.NewSimpleSchema("dashvalidator.ext.grafana.com", "v1alpha1", NewDashboardCompatibilityScore(), &DashboardCompatibilityScoreList{}, resource.WithKind("DashboardCompatibilityScore"),
resource.WithPlural("dashboardcompatibilityscores"), resource.WithScope(resource.NamespacedScope))
kindDashboardCompatibilityScore = resource.Kind{
Schema: schemaDashboardCompatibilityScore,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &JSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func Kind() resource.Kind {
return kindDashboardCompatibilityScore
}
// Schema returns a resource.SimpleSchema representation of DashboardCompatibilityScore
func Schema() *resource.SimpleSchema {
return schemaDashboardCompatibilityScore
}
// Interface compliance checks
var _ resource.Schema = kindDashboardCompatibilityScore

View File

@@ -0,0 +1,48 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
// DataSourceMapping specifies a datasource to validate dashboard queries against.
// Maps logical datasource references in the dashboard to actual datasource instances.
// +k8s:openapi-gen=true
type DataSourceMapping struct {
// Unique identifier of the datasource instance.
// Example: "prometheus-prod-us-west"
Uid string `json:"uid"`
// Type of datasource plugin.
// MVP: Only "prometheus" supported.
// Future: "mysql", "postgres", "elasticsearch", etc.
Type string `json:"type"`
// Optional human-readable name for display in results.
// If not provided, UID will be used in error messages.
// Example: "Production Prometheus (US-West)"
Name *string `json:"name,omitempty"`
}
// NewDataSourceMapping creates a new DataSourceMapping object.
func NewDataSourceMapping() *DataSourceMapping {
return &DataSourceMapping{}
}
// +k8s:openapi-gen=true
type Spec struct {
// Complete dashboard JSON object to validate.
// Must be a v1 dashboard schema (contains "panels" array).
// v2 dashboards (with "elements" structure) are not yet supported.
DashboardJson map[string]interface{} `json:"dashboardJson"`
// Array of datasources to validate against.
// The validator will check dashboard queries against each datasource
// and provide per-datasource compatibility results.
//
// MVP: Only single datasource supported (array length = 1), Prometheus type only.
// Future: Will support multiple datasources for dashboards with mixed queries.
DatasourceMappings []DataSourceMapping `json:"datasourceMappings"`
}
// NewSpec creates a new Spec object.
func NewSpec() *Spec {
return &Spec{
DashboardJson: map[string]interface{}{},
DatasourceMappings: []DataSourceMapping{},
}
}

View File

@@ -0,0 +1,151 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v1alpha1
// DataSourceResult contains validation results for a single datasource.
// Provides aggregate statistics and per-query breakdown of compatibility.
// +k8s:openapi-gen=true
type DataSourceResult struct {
// Datasource UID that was validated (matches DataSourceMapping.uid)
Uid string `json:"uid"`
// Datasource type (matches DataSourceMapping.type)
Type string `json:"type"`
// Optional display name (matches DataSourceMapping.name if provided)
Name *string `json:"name,omitempty"`
// Total number of queries in the dashboard targeting this datasource.
// Includes all panel targets/queries that reference this datasource.
TotalQueries int64 `json:"totalQueries"`
// Number of queries successfully validated.
// May be less than totalQueries if some queries couldn't be parsed.
CheckedQueries int64 `json:"checkedQueries"`
// Total number of unique metrics/identifiers referenced across all queries.
// For Prometheus: metric names extracted from PromQL expressions.
// For SQL datasources: table and column names.
TotalMetrics int64 `json:"totalMetrics"`
// Number of metrics that exist in the datasource schema.
// foundMetrics <= totalMetrics
FoundMetrics int64 `json:"foundMetrics"`
// Array of metric names that were referenced but don't exist.
// Useful for debugging why a dashboard shows "no data".
// Example for Prometheus: ["http_requests_total", "api_latency_seconds"]
MissingMetrics []string `json:"missingMetrics"`
// Per-query breakdown showing which specific queries have issues.
// One entry per query target (refId: "A", "B", "C", etc.) in each panel.
// Allows pinpointing exactly which panel/query needs fixing.
QueryBreakdown []QueryBreakdown `json:"queryBreakdown"`
// Overall compatibility score for this datasource (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// Used to calculate the global compatibilityScore in status.
CompatibilityScore float64 `json:"compatibilityScore"`
}
// NewDataSourceResult creates a new DataSourceResult object.
func NewDataSourceResult() *DataSourceResult {
return &DataSourceResult{
MissingMetrics: []string{},
QueryBreakdown: []QueryBreakdown{},
}
}
// QueryBreakdown provides compatibility details for a single query within a panel.
// Granular per-query results allow users to identify exactly which queries need fixing.
//
// Note: A panel can have multiple queries (refId: "A", "B", "C", etc.),
// so there may be multiple QueryBreakdown entries for the same panelID.
// +k8s:openapi-gen=true
type QueryBreakdown struct {
// Human-readable panel title for context.
// Example: "CPU Usage", "Request Rate"
PanelTitle string `json:"panelTitle"`
// Numeric panel ID from dashboard JSON.
// Used to correlate with dashboard structure.
PanelID int64 `json:"panelID"`
// Query identifier within the panel.
// Values: "A", "B", "C", etc. (from panel.targets[].refId)
// Uniquely identifies which query in a multi-query panel this refers to.
QueryRefId string `json:"queryRefId"`
// Number of unique metrics referenced in this specific query.
// For Prometheus: metrics extracted from the PromQL expr.
// Example: rate(http_requests_total[5m]) references 1 metric.
TotalMetrics int64 `json:"totalMetrics"`
// Number of those metrics that exist in the datasource.
// foundMetrics <= totalMetrics
FoundMetrics int64 `json:"foundMetrics"`
// Array of missing metric names specific to this query.
// Helps identify exactly which part of a query expression will fail.
// Empty array means query is fully compatible.
MissingMetrics []string `json:"missingMetrics"`
// Compatibility percentage for this individual query (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// 100 = query will work perfectly, 0 = query will return no data.
CompatibilityScore float64 `json:"compatibilityScore"`
}
// NewQueryBreakdown creates a new QueryBreakdown object.
func NewQueryBreakdown() *QueryBreakdown {
return &QueryBreakdown{
MissingMetrics: []string{},
}
}
// +k8s:openapi-gen=true
type StatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State StatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewStatusOperatorState creates a new StatusOperatorState object.
func NewStatusOperatorState() *StatusOperatorState {
return &StatusOperatorState{}
}
// +k8s:openapi-gen=true
type Status struct {
// Overall compatibility score across all datasources (0-100).
// Calculated as: (total found metrics / total referenced metrics) * 100
//
// Score interpretation:
// - 100: Perfect compatibility, all queries will work
// - 80-99: Excellent, minor missing metrics
// - 50-79: Fair, significant missing metrics
// - 0-49: Poor, most queries will fail
CompatibilityScore float64 `json:"compatibilityScore"`
// Per-datasource validation results.
// Array length matches spec.datasourceMappings.
// Each element contains detailed metrics and query-level breakdown.
DatasourceResults []DataSourceResult `json:"datasourceResults"`
// ISO 8601 timestamp of when validation was last performed.
// Example: "2024-01-15T10:30:00Z"
LastChecked *string `json:"lastChecked,omitempty"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"`
// Human-readable summary of validation result.
// Examples: "All queries compatible", "3 missing metrics found"
Message *string `json:"message,omitempty"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewStatus creates a new Status object.
func NewStatus() *Status {
return &Status{
DatasourceResults: []DataSourceResult{},
}
}
// +k8s:openapi-gen=true
type StatusOperatorStateState string
const (
StatusOperatorStateStateSuccess StatusOperatorStateState = "success"
StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress"
StatusOperatorStateStateFailed StatusOperatorStateState = "failed"
)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,568 @@
package validator
import (
"context"
"fmt"
"net/http"
"strings"
)
// DashboardCompatibilityRequest contains the dashboard and datasource mappings to validate
type DashboardCompatibilityRequest struct {
DashboardJSON map[string]interface{} // Dashboard JSON structure
DatasourceMappings []DatasourceMapping // List of datasources to validate against
}
// DatasourceMapping maps a datasource UID to its type and optionally name/URL
type DatasourceMapping struct {
UID string // Datasource UID
Type string // Datasource type (prometheus, mysql, etc.)
Name string // Optional: Datasource name
URL string // Datasource URL
HTTPClient *http.Client // Authenticated HTTP client
}
// DashboardCompatibilityResult contains the validation results for a dashboard
type DashboardCompatibilityResult struct {
CompatibilityScore float64 // Overall compatibility (0.0 - 1.0)
DatasourceResults []DatasourceValidationResult // Per-datasource results
}
// DatasourceValidationResult contains validation results for one datasource
type DatasourceValidationResult struct {
UID string
Type string
Name string
TotalQueries int
CheckedQueries int
TotalMetrics int
FoundMetrics int
MissingMetrics []string
QueryBreakdown []QueryResult
CompatibilityScore float64
}
// ValidateDashboardCompatibility is the main entry point for validating dashboard compatibility
// It extracts queries from the dashboard, validates them against each datasource, and returns aggregated results
func ValidateDashboardCompatibility(ctx context.Context, req DashboardCompatibilityRequest) (*DashboardCompatibilityResult, error) {
// MVP: Only support single datasource validation
if len(req.DatasourceMappings) != 1 {
return nil, fmt.Errorf("MVP only supports single datasource validation, got %d datasources", len(req.DatasourceMappings))
}
singleDatasource := req.DatasourceMappings[0]
result := &DashboardCompatibilityResult{
DatasourceResults: make([]DatasourceValidationResult, 0, len(req.DatasourceMappings)),
}
// Step 1: Extract queries from dashboard JSON
queries, err := extractQueriesFromDashboard(req.DashboardJSON)
if err != nil {
return nil, fmt.Errorf("failed to extract queries from dashboard: %w", err)
}
fmt.Printf("[DEBUG] Extracted %d queries from dashboard\n", len(queries))
for i, q := range queries {
fmt.Printf("[DEBUG] Query %d: DS=%s, RefID=%s, Query=%s\n", i, q.DatasourceUID, q.RefID, q.QueryText)
}
// Step 2: Group queries by datasource UID (with variable resolution for MVP)
queriesByDatasource := groupQueriesByDatasource(queries, singleDatasource.UID, req.DashboardJSON)
fmt.Printf("[DEBUG] Grouped queries by %d datasources\n", len(queriesByDatasource))
for dsUID, dsQueries := range queriesByDatasource {
fmt.Printf("[DEBUG] Datasource %s has %d queries\n", dsUID, len(dsQueries))
}
// Step 3: Validate each datasource
var totalCompatibility float64
validatedCount := 0
for _, dsMapping := range req.DatasourceMappings {
fmt.Printf("[DEBUG] Processing datasource mapping: UID=%s, Type=%s, URL=%s\n", dsMapping.UID, dsMapping.Type, dsMapping.URL)
// Get queries for this datasource
dsQueries, ok := queriesByDatasource[dsMapping.UID]
if !ok || len(dsQueries) == 0 {
// No queries for this datasource, skip
fmt.Printf("[DEBUG] No queries found for datasource %s, skipping\n", dsMapping.UID)
continue
}
fmt.Printf("[DEBUG] Found %d queries for datasource %s\n", len(dsQueries), dsMapping.UID)
// Get validator for this datasource type
v, err := GetValidator(dsMapping.Type)
if err != nil {
// Unsupported datasource type, skip but log
fmt.Printf("[DEBUG] Failed to get validator for type %s: %v\n", dsMapping.Type, err)
continue
}
fmt.Printf("[DEBUG] Got validator for type %s, starting validation\n", dsMapping.Type)
// Build Datasource struct
ds := Datasource{
UID: dsMapping.UID,
Type: dsMapping.Type,
Name: dsMapping.Name,
URL: dsMapping.URL,
HTTPClient: dsMapping.HTTPClient,
}
// Validate queries
validationResult, err := v.ValidateQueries(ctx, dsQueries, ds)
if err != nil {
// Validation failed for this datasource - return error to caller
// This could be a connection error, auth error, or other critical failure
return nil, fmt.Errorf("validation failed for datasource %s: %w", dsMapping.UID, err)
}
// Convert to DatasourceValidationResult
dsResult := DatasourceValidationResult{
UID: dsMapping.UID,
Type: dsMapping.Type,
Name: dsMapping.Name,
TotalQueries: validationResult.TotalQueries,
CheckedQueries: validationResult.CheckedQueries,
TotalMetrics: validationResult.TotalMetrics,
FoundMetrics: validationResult.FoundMetrics,
MissingMetrics: validationResult.MissingMetrics,
QueryBreakdown: validationResult.QueryBreakdown,
CompatibilityScore: validationResult.CompatibilityScore,
}
result.DatasourceResults = append(result.DatasourceResults, dsResult)
totalCompatibility += validationResult.CompatibilityScore
validatedCount++
}
// Step 4: Calculate overall compatibility score
if validatedCount > 0 {
result.CompatibilityScore = totalCompatibility / float64(validatedCount)
} else {
result.CompatibilityScore = 1.0 // No datasources = perfect compatibility
}
return result, nil
}
// extractQueriesFromDashboard parses the dashboard JSON and extracts all queries
// Supports both v1 (legacy) and v2 (new) dashboard formats
func extractQueriesFromDashboard(dashboardJSON map[string]interface{}) ([]DashboardQuery, error) {
var queries []DashboardQuery
// Debug: Print what keys we have
fmt.Printf("[DEBUG] Dashboard JSON keys: ")
for key := range dashboardJSON {
fmt.Printf("%s, ", key)
}
fmt.Printf("\n")
// Detect dashboard version (v1 uses "panels", v2 uses different structure)
// For MVP, we only support v1 (legacy format with panels array)
if !isV1Dashboard(dashboardJSON) {
fmt.Printf("[DEBUG] isV1Dashboard returned false, 'panels' key exists: %v\n", dashboardJSON["panels"] != nil)
return nil, fmt.Errorf("unsupported dashboard format: only v1 dashboards are supported in MVP")
}
// Extract panels array
panels, ok := dashboardJSON["panels"].([]interface{})
if !ok {
// No panels in dashboard, return empty array
return queries, nil
}
// Iterate through all panels
for _, panelInterface := range panels {
panel, ok := panelInterface.(map[string]interface{})
if !ok {
continue
}
// Extract queries from this panel
panelQueries := extractQueriesFromPanel(panel)
queries = append(queries, panelQueries...)
// Handle nested panels in collapsed rows
nestedPanels, hasNested := panel["panels"].([]interface{})
if hasNested {
for _, nestedPanelInterface := range nestedPanels {
nestedPanel, ok := nestedPanelInterface.(map[string]interface{})
if !ok {
continue
}
nestedQueries := extractQueriesFromPanel(nestedPanel)
queries = append(queries, nestedQueries...)
}
}
}
return queries, nil
}
// isV1Dashboard checks if a dashboard is in v1 (legacy) format
// v1 dashboards have a "panels" array at the top level
// v2 dashboards have "elements" map and "layout" structure
//
// This follows Grafana's official dashboard conversion logic which uses
// type-safe assertions to distinguish between formats.
// Reference: apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go:450
func isV1Dashboard(dashboard map[string]interface{}) bool {
// Check for v2 indicators first (positive identification)
// v2 dashboards use a map of elements, not an array
if _, hasElements := dashboard["elements"].(map[string]interface{}); hasElements {
return false // Definitely v2
}
// v2 dashboards also have a layout structure
if _, hasLayout := dashboard["layout"]; hasLayout {
return false // v2 has layout field
}
// Check for v1 panels with type assertion (must be an array)
// This is type-safe: `{"panels": "string"}` would fail this check and return false
_, hasPanels := dashboard["panels"].([]interface{})
return hasPanels
}
// extractQueriesFromPanel extracts all queries/targets from a single panel
func extractQueriesFromPanel(panel map[string]interface{}) []DashboardQuery {
var queries []DashboardQuery
// Get panel info for context
panelTitle := getStringValue(panel, "title", "Untitled Panel")
panelID := getIntValue(panel, "id", 0)
// Extract targets array (queries)
targets, hasTargets := panel["targets"].([]interface{})
if !hasTargets {
return queries
}
// Iterate through each target/query
for _, targetInterface := range targets {
target, ok := targetInterface.(map[string]interface{})
if !ok {
continue
}
// Extract datasource UID
datasourceUID := extractDatasourceUID(target, panel)
if datasourceUID == "" {
// Skip queries without datasource
continue
}
// Extract query text (different fields for different datasources)
queryText := extractQueryText(target)
if queryText == "" {
// Skip empty queries
continue
}
// Extract refId (A, B, C, etc.)
refID := getStringValue(target, "refId", "")
// Build DashboardQuery
query := DashboardQuery{
DatasourceUID: datasourceUID,
RefID: refID,
QueryText: queryText,
PanelTitle: panelTitle,
PanelID: panelID,
}
queries = append(queries, query)
}
return queries
}
// extractDatasourceUID gets the datasource UID from a target, falling back to panel datasource
func extractDatasourceUID(target map[string]interface{}, panel map[string]interface{}) string {
// Try target-level datasource first
if ds, ok := target["datasource"]; ok {
if uid := getDatasourceUIDFromValue(ds); uid != "" {
return uid
}
}
// Fall back to panel-level datasource
if ds, ok := panel["datasource"]; ok {
if uid := getDatasourceUIDFromValue(ds); uid != "" {
return uid
}
}
return ""
}
// getDatasourceUIDFromValue extracts UID from datasource value (can be string or object)
func getDatasourceUIDFromValue(ds interface{}) string {
switch v := ds.(type) {
case string:
// Direct UID string
return v
case map[string]interface{}:
// Structured datasource reference { uid: "...", type: "..." }
return getStringValue(v, "uid", "")
default:
return ""
}
}
// isVariableReference checks if a string is a template variable reference
// Matches patterns: ${varname}, $varname, [[varname]]
// Follows Grafana's frontend regex: /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g
// where \w = [A-Za-z0-9_] (alphanumeric + underscore, NO dashes)
func isVariableReference(uid string) bool {
if uid == "" {
return false
}
// Match ${...} pattern - requires at least one \w character inside braces
if len(uid) > 3 && uid[0] == '$' && uid[1] == '{' && uid[len(uid)-1] == '}' {
// Extract content between ${ and }
content := uid[2 : len(uid)-1]
if len(content) == 0 {
return false // Empty braces ${} not allowed
}
// Check if content starts with \w+ (before any . or :)
for i, ch := range content {
if ch == '.' || ch == ':' {
// Found delimiter, check if we had at least one \w before it
return i > 0
}
// Must be alphanumeric or underscore
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '_') {
return false
}
}
return true // All characters were valid \w
}
// Match $varname pattern - requires at least one \w character after $
// \w = alphanumeric + underscore (digits ARE allowed, dashes are NOT)
if uid[0] == '$' && len(uid) > 1 {
for i := 1; i < len(uid); i++ {
ch := uid[i]
// \w = [A-Za-z0-9_] only (NO dashes)
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '_') {
return false
}
}
return true
}
// Match [[varname]] pattern - requires at least one \w character inside brackets
// Also supports [[varname:format]] syntax
if len(uid) > 4 && uid[0] == '[' && uid[1] == '[' &&
uid[len(uid)-2] == ']' && uid[len(uid)-1] == ']' {
// Extract content between [[ and ]]
content := uid[2 : len(uid)-2]
if len(content) == 0 {
return false // Empty brackets [[]] not allowed
}
// Check if content starts with \w+ (before any :)
for i, ch := range content {
if ch == ':' {
// Found format delimiter, check if we had at least one \w before it
return i > 0
}
// Must be alphanumeric or underscore
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '_') {
return false
}
}
return true // All characters were valid \w
}
return false
}
// extractVariableName extracts the variable name from a variable reference
// Returns only the name part, excluding fieldPath (after .) and format (after :)
// Examples: ${var.field} -> "var", [[var:text]] -> "var", $datasource -> "datasource"
func extractVariableName(varRef string) string {
if !isVariableReference(varRef) {
return ""
}
// Handle ${varname} pattern - may include .fieldPath or :format
if len(varRef) > 3 && varRef[0] == '$' && varRef[1] == '{' && varRef[len(varRef)-1] == '}' {
content := varRef[2 : len(varRef)-1]
// Extract only up to . or :
for i, ch := range content {
if ch == '.' || ch == ':' {
return content[:i]
}
}
return content
}
// Handle $varname pattern - no modifiers possible
if varRef[0] == '$' && len(varRef) > 1 {
return varRef[1:]
}
// Handle [[varname]] pattern - may include :format
if len(varRef) > 4 && varRef[0] == '[' && varRef[1] == '[' {
content := varRef[2 : len(varRef)-2]
// Extract only up to :
for i, ch := range content {
if ch == ':' {
return content[:i]
}
}
return content
}
return ""
}
// isPrometheusVariable checks if a variable reference points to a Prometheus datasource
// Looks in dashboard.__inputs for the datasource type
func isPrometheusVariable(varRef string, dashboardJSON map[string]interface{}) bool {
if !isVariableReference(varRef) {
return false
}
varName := extractVariableName(varRef)
if varName == "" {
return false
}
// Look for __inputs array in dashboard
inputs, hasInputs := dashboardJSON["__inputs"].([]interface{})
if !hasInputs {
// No __inputs, assume it might be Prometheus (MVP: single datasource)
// This is a fallback for dashboards without explicit __inputs
return true
}
// Search for this variable in __inputs
for _, inputInterface := range inputs {
input, ok := inputInterface.(map[string]interface{})
if !ok {
continue
}
// Check if this input matches our variable name
inputName := getStringValue(input, "name", "")
inputType := getStringValue(input, "type", "")
inputPluginID := getStringValue(input, "pluginId", "")
// Match by name (case-insensitive for flexibility)
if inputName != "" && varName != "" {
if inputName == varName ||
strings.EqualFold(inputName, varName) ||
strings.Contains(strings.ToLower(varName), strings.ToLower(inputName)) {
// Check if it's a datasource input with prometheus plugin
if inputType == "datasource" && inputPluginID == "prometheus" {
return true
}
}
}
}
// Not found or not Prometheus
return false
}
// resolveDatasourceUID resolves a datasource UID, handling variable references (MVP: single datasource)
// For MVP, all Prometheus variables resolve to the single datasource UID
func resolveDatasourceUID(uid string, singleDatasourceUID string, dashboardJSON map[string]interface{}) string {
// If not a variable, return as-is (concrete UID)
if !isVariableReference(uid) {
return uid
}
// Check if it's a Prometheus variable
if isPrometheusVariable(uid, dashboardJSON) {
fmt.Printf("[DEBUG] Resolved Prometheus variable %s to %s\n", uid, singleDatasourceUID)
return singleDatasourceUID
}
// Non-Prometheus variable, return as-is (will be ignored in grouping)
fmt.Printf("[DEBUG] Variable %s is not a Prometheus variable, skipping\n", uid)
return uid
}
// extractQueryText extracts the query text from a target
// Different datasources use different field names (expr, query, rawSql, etc.)
func extractQueryText(target map[string]interface{}) string {
// Try common query field names
queryFields := []string{"expr", "query", "rawSql", "rawQuery", "target", "measurement"}
for _, field := range queryFields {
if queryText := getStringValue(target, field, ""); queryText != "" {
return queryText
}
}
return ""
}
// getStringValue safely extracts a string value from a map
func getStringValue(m map[string]interface{}, key string, defaultValue string) string {
if value, ok := m[key]; ok {
if s, ok := value.(string); ok {
return s
}
}
return defaultValue
}
// getIntValue safely extracts an int value from a map
func getIntValue(m map[string]interface{}, key string, defaultValue int) int {
if value, ok := m[key]; ok {
switch v := value.(type) {
case int:
return v
case float64:
return int(v)
case int64:
return int(v)
}
}
return defaultValue
}
// DashboardQuery represents a query extracted from a dashboard panel
type DashboardQuery struct {
DatasourceUID string // Which datasource this query belongs to
RefID string // Query reference ID
QueryText string // The actual query
PanelTitle string // Panel title
PanelID int // Panel ID
}
// groupQueriesByDatasource groups dashboard queries by their datasource UID
// For MVP: resolves Prometheus template variables to the single datasource UID
func groupQueriesByDatasource(queries []DashboardQuery, singleDatasourceUID string, dashboardJSON map[string]interface{}) map[string][]Query {
grouped := make(map[string][]Query)
for _, dq := range queries {
q := Query{
RefID: dq.RefID,
QueryText: dq.QueryText,
PanelTitle: dq.PanelTitle,
PanelID: dq.PanelID,
}
// Resolve datasource UID (handles both concrete UIDs and variables)
resolvedUID := resolveDatasourceUID(dq.DatasourceUID, singleDatasourceUID, dashboardJSON)
// Only add to grouping if we got a valid resolved UID
if resolvedUID != "" {
grouped[resolvedUID] = append(grouped[resolvedUID], q)
}
}
return grouped
}

View File

@@ -0,0 +1,604 @@
package validator
import (
"testing"
"github.com/stretchr/testify/require"
)
// Note: extractQueryText() uses a hardcoded field priority list because
// Grafana doesn't expose datasource query schemas at runtime.
// When Grafana adds new datasource types, update the list in dashboard.go
// and add corresponding test cases here.
// =============================================================================
// Category 1: extractQueryText Tests
// Tests verify the hardcoded field priority list works correctly.
// =============================================================================
func TestExtractQueryText(t *testing.T) {
tests := []struct {
name string
target map[string]interface{}
expected string
}{
{
name: "prometheus_expr_field",
target: map[string]interface{}{
"expr": "up",
},
expected: "up",
},
{
name: "mysql_rawSql_field",
target: map[string]interface{}{
"rawSql": "SELECT * FROM users LIMIT 100",
},
expected: "SELECT * FROM users LIMIT 100",
},
{
name: "generic_query_field",
target: map[string]interface{}{
"query": "show measurements",
},
expected: "show measurements",
},
{
name: "field_priority_order",
target: map[string]interface{}{
"expr": "rate(cpu[5m])", // First priority
"query": "show metrics", // Second priority
},
expected: "rate(cpu[5m])", // Should return expr, not query
},
{
name: "missing_query_fields",
target: map[string]interface{}{"refId": "A", "hide": false},
expected: "",
},
{
name: "empty_string_value",
target: map[string]interface{}{
"expr": "",
},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractQueryText(tt.target)
require.Equal(t, tt.expected, result)
})
}
}
// =============================================================================
// Category 2: getDatasourceUIDFromValue Tests (4 tests)
// =============================================================================
func TestGetDatasourceUIDFromValue(t *testing.T) {
tests := []struct {
name string
value interface{}
expected string
}{
{
name: "string_datasource_uid",
value: "prom-123",
expected: "prom-123",
},
{
name: "object_datasource_with_uid",
value: map[string]interface{}{
"uid": "prom-123",
"type": "prometheus",
},
expected: "prom-123",
},
{
name: "variable_reference_passed_through",
value: "${DS_PROMETHEUS}",
expected: "${DS_PROMETHEUS}",
},
{
name: "nil_value",
value: nil,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getDatasourceUIDFromValue(tt.value)
require.Equal(t, tt.expected, result)
})
}
}
// =============================================================================
// Category 3: extractDatasourceUID Tests (5 tests)
// =============================================================================
func TestExtractDatasourceUID(t *testing.T) {
tests := []struct {
name string
target map[string]interface{}
panel map[string]interface{}
expected string
}{
{
name: "target_level_datasource_string",
target: map[string]interface{}{
"datasource": "target-ds-123",
},
panel: map[string]interface{}{},
expected: "target-ds-123",
},
{
name: "target_level_datasource_object",
target: map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "target-ds-456",
"type": "prometheus",
},
},
panel: map[string]interface{}{},
expected: "target-ds-456",
},
{
name: "panel_level_fallback",
target: map[string]interface{}{},
panel: map[string]interface{}{
"datasource": "panel-ds-789",
},
expected: "panel-ds-789",
},
{
name: "target_level_takes_precedence",
target: map[string]interface{}{
"datasource": "target-ds",
},
panel: map[string]interface{}{
"datasource": "panel-ds",
},
expected: "target-ds",
},
{
name: "both_missing_returns_empty",
target: map[string]interface{}{},
panel: map[string]interface{}{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractDatasourceUID(tt.target, tt.panel)
require.Equal(t, tt.expected, result)
})
}
}
// =============================================================================
// Category 4: extractQueriesFromPanel Tests (8 tests)
// =============================================================================
func TestExtractQueriesFromPanel(t *testing.T) {
tests := []struct {
name string
panel map[string]interface{}
expected []DashboardQuery
}{
{
name: "panel_with_single_target",
panel: map[string]interface{}{
"id": 42,
"title": "CPU Usage",
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
"expr": "rate(cpu[5m])",
"datasource": "prom-main",
},
},
},
expected: []DashboardQuery{
{
DatasourceUID: "prom-main",
RefID: "A",
QueryText: "rate(cpu[5m])",
PanelTitle: "CPU Usage",
PanelID: 42,
},
},
},
{
name: "panel_with_multiple_targets",
panel: map[string]interface{}{
"id": 10,
"title": "Metrics",
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
"expr": "up",
"datasource": "prom-1",
},
map[string]interface{}{
"refId": "B",
"expr": "down",
"datasource": "prom-1",
},
},
},
expected: []DashboardQuery{
{
DatasourceUID: "prom-1",
RefID: "A",
QueryText: "up",
PanelTitle: "Metrics",
PanelID: 10,
},
{
DatasourceUID: "prom-1",
RefID: "B",
QueryText: "down",
PanelTitle: "Metrics",
PanelID: 10,
},
},
},
{
name: "panel_with_no_targets_field",
panel: map[string]interface{}{
"id": 1,
"title": "Text Panel",
},
expected: []DashboardQuery{},
},
{
name: "panel_with_empty_targets_array",
panel: map[string]interface{}{
"id": 2,
"title": "Empty",
"targets": []interface{}{},
},
expected: []DashboardQuery{},
},
{
name: "target_missing_datasource_skipped",
panel: map[string]interface{}{
"id": 3,
"title": "Incomplete",
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
"expr": "up",
// No datasource field
},
},
},
expected: []DashboardQuery{}, // Empty because no datasource
},
{
name: "target_missing_query_text_skipped",
panel: map[string]interface{}{
"id": 4,
"title": "No Query",
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
"datasource": "prom-1",
// No expr/query field
},
},
},
expected: []DashboardQuery{}, // Empty because no query text
},
{
name: "panel_metadata_extraction",
panel: map[string]interface{}{
"id": 999,
"title": "Custom Title",
"targets": []interface{}{
map[string]interface{}{
"refId": "Z",
"expr": "test_metric",
"datasource": "ds-abc",
},
},
},
expected: []DashboardQuery{
{
DatasourceUID: "ds-abc",
RefID: "Z",
QueryText: "test_metric",
PanelTitle: "Custom Title",
PanelID: 999,
},
},
},
{
name: "panel_id_as_float64",
panel: map[string]interface{}{
"id": float64(123), // JSON numbers parse as float64
"title": "Float ID Panel",
"targets": []interface{}{
map[string]interface{}{
"refId": "A",
"expr": "metric",
"datasource": "ds-1",
},
},
},
expected: []DashboardQuery{
{
DatasourceUID: "ds-1",
RefID: "A",
QueryText: "metric",
PanelTitle: "Float ID Panel",
PanelID: 123,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractQueriesFromPanel(tt.panel)
if len(tt.expected) == 0 {
require.Empty(t, result)
} else {
require.Equal(t, tt.expected, result)
}
})
}
}
// =============================================================================
// Category 5: Helper Functions Tests
// =============================================================================
func TestGetStringValue(t *testing.T) {
tests := []struct {
name string
m map[string]interface{}
key string
defaultValue string
expected string
}{
{
name: "returns_value_if_exists",
m: map[string]interface{}{"name": "test"},
key: "name",
defaultValue: "default",
expected: "test",
},
{
name: "returns_default_if_missing",
m: map[string]interface{}{"other": "value"},
key: "name",
defaultValue: "default",
expected: "default",
},
{
name: "handles_non_string_type",
m: map[string]interface{}{"name": 123},
key: "name",
defaultValue: "default",
expected: "default",
},
{
name: "empty_map_returns_default",
m: map[string]interface{}{},
key: "name",
defaultValue: "default",
expected: "default",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getStringValue(tt.m, tt.key, tt.defaultValue)
require.Equal(t, tt.expected, result)
})
}
}
func TestGetIntValue(t *testing.T) {
tests := []struct {
name string
m map[string]interface{}
key string
defaultValue int
expected int
}{
{
name: "returns_int_value",
m: map[string]interface{}{"count": 42},
key: "count",
defaultValue: 0,
expected: 42,
},
{
name: "handles_float64_conversion",
m: map[string]interface{}{"count": float64(123)},
key: "count",
defaultValue: 0,
expected: 123,
},
{
name: "handles_int64_conversion",
m: map[string]interface{}{"count": int64(456)},
key: "count",
defaultValue: 0,
expected: 456,
},
{
name: "returns_default_for_missing",
m: map[string]interface{}{},
key: "count",
defaultValue: 99,
expected: 99,
},
{
name: "returns_default_for_invalid_type",
m: map[string]interface{}{"count": "not a number"},
key: "count",
defaultValue: 99,
expected: 99,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getIntValue(tt.m, tt.key, tt.defaultValue)
require.Equal(t, tt.expected, result)
})
}
}
// =============================================================================
// Category 6: Integration Tests
// Real-world dashboard panel structures
// =============================================================================
func TestRealisticPrometheusPanel(t *testing.T) {
// Realistic Prometheus panel from actual Grafana dashboard
panel := map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "prometheus-main",
},
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": 0,
"y": 0,
},
"id": 28,
"title": "Request Rate",
"type": "timeseries",
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "prometheus-main",
},
"expr": "rate(http_requests_total{job=\"api\"}[5m])",
"refId": "A",
"legendFormat": "{{method}} {{status}}",
"interval": "",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "prometheus-main",
},
"expr": "rate(http_requests_total{job=\"worker\"}[5m])",
"refId": "B",
"legendFormat": "{{method}}",
},
},
}
result := extractQueriesFromPanel(panel)
require.Len(t, result, 2)
require.Equal(t, "prometheus-main", result[0].DatasourceUID)
require.Equal(t, "A", result[0].RefID)
require.Equal(t, "rate(http_requests_total{job=\"api\"}[5m])", result[0].QueryText)
require.Equal(t, "Request Rate", result[0].PanelTitle)
require.Equal(t, 28, result[0].PanelID)
require.Equal(t, "prometheus-main", result[1].DatasourceUID)
require.Equal(t, "B", result[1].RefID)
require.Equal(t, "rate(http_requests_total{job=\"worker\"}[5m])", result[1].QueryText)
}
func TestRealisticMySQLPanel(t *testing.T) {
// Realistic MySQL panel structure
panel := map[string]interface{}{
"id": 10,
"title": "Recent Users",
"type": "table",
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "mysql-prod",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "mysql",
"uid": "mysql-prod",
},
"refId": "A",
"rawSql": "SELECT id, username, email FROM users WHERE created_at > NOW() - INTERVAL 1 DAY ORDER BY created_at DESC LIMIT 100",
"format": "table",
},
},
}
result := extractQueriesFromPanel(panel)
require.Len(t, result, 1)
require.Equal(t, "mysql-prod", result[0].DatasourceUID)
require.Equal(t, "A", result[0].RefID)
require.Contains(t, result[0].QueryText, "SELECT id, username, email FROM users")
require.Equal(t, "Recent Users", result[0].PanelTitle)
require.Equal(t, 10, result[0].PanelID)
}
func TestMixedDatasourcesPanel(t *testing.T) {
// Panel with targets using different datasource types
panel := map[string]interface{}{
"id": 50,
"title": "Mixed Data",
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "default-prom",
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "prom-1",
},
"refId": "A",
"expr": "up",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "elasticsearch",
"uid": "elastic-1",
},
"refId": "B",
"query": "status:200",
},
map[string]interface{}{
// Uses panel-level datasource (fallback)
"refId": "C",
"expr": "down",
},
},
}
result := extractQueriesFromPanel(panel)
require.Len(t, result, 3)
// Prometheus query
require.Equal(t, "prom-1", result[0].DatasourceUID)
require.Equal(t, "A", result[0].RefID)
require.Equal(t, "up", result[0].QueryText)
// Elasticsearch query
require.Equal(t, "elastic-1", result[1].DatasourceUID)
require.Equal(t, "B", result[1].RefID)
require.Equal(t, "status:200", result[1].QueryText)
// Query with panel-level datasource fallback
require.Equal(t, "default-prom", result[2].DatasourceUID)
require.Equal(t, "C", result[2].RefID)
require.Equal(t, "down", result[2].QueryText)
}

View File

@@ -0,0 +1,197 @@
package validator
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIsV1Dashboard(t *testing.T) {
tests := []struct {
name string
dashboard map[string]interface{}
expected bool
}{
{
name: "v1 dashboard with panels array",
dashboard: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"title": "Panel 1",
"type": "timeseries",
},
},
},
expected: true,
},
{
name: "v1 dashboard with empty panels",
dashboard: map[string]interface{}{
"panels": []interface{}{},
},
expected: true,
},
{
name: "v2 dashboard with elements map",
dashboard: map[string]interface{}{
"elements": map[string]interface{}{
"panel-1": map[string]interface{}{
"kind": "Panel",
"spec": map[string]interface{}{
"id": 1,
"title": "Panel 1",
},
},
},
},
expected: false,
},
{
name: "v2 dashboard with layout",
dashboard: map[string]interface{}{
"layout": map[string]interface{}{
"kind": "GridLayout",
"spec": map[string]interface{}{
"items": []interface{}{},
},
},
},
expected: false,
},
{
name: "v2 dashboard with both elements and layout",
dashboard: map[string]interface{}{
"elements": map[string]interface{}{
"panel-1": map[string]interface{}{
"kind": "Panel",
},
},
"layout": map[string]interface{}{
"kind": "GridLayout",
},
},
expected: false,
},
{
name: "empty dashboard",
dashboard: map[string]interface{}{},
expected: false,
},
{
name: "dashboard with wrong panels type (string instead of array)",
dashboard: map[string]interface{}{
"panels": "this-should-be-array-not-string",
},
expected: false,
},
{
name: "dashboard with other fields only",
dashboard: map[string]interface{}{
"title": "Test Dashboard",
"uid": "test-uid",
"tags": []string{"monitoring"},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isV1Dashboard(tt.dashboard)
require.Equal(t, tt.expected, result, "isV1Dashboard() returned unexpected result")
})
}
}
func TestExtractQueriesFromDashboard_VersionValidation(t *testing.T) {
tests := []struct {
name string
dashboard map[string]interface{}
expectError bool
errorContains string
}{
{
name: "valid v1 dashboard extracts queries successfully",
dashboard: map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": 1,
"title": "CPU Usage",
"type": "timeseries",
"gridPos": map[string]interface{}{
"h": 8,
"w": 12,
"x": 0,
"y": 0,
},
"targets": []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"type": "prometheus",
"uid": "test-prometheus",
},
"expr": "rate(cpu_usage_total[5m])",
"refId": "A",
},
},
},
},
},
expectError: false,
},
{
name: "v2 dashboard returns unsupported format error",
dashboard: map[string]interface{}{
"elements": map[string]interface{}{
"panel-1": map[string]interface{}{
"kind": "Panel",
"spec": map[string]interface{}{
"id": 1,
"title": "Panel 1",
"data": map[string]interface{}{
"kind": "QueryGroup",
},
"vizConfig": map[string]interface{}{
"kind": "TimeSeriesVisualConfig",
"pluginId": "timeseries",
},
},
},
},
"layout": map[string]interface{}{
"kind": "GridLayout",
"spec": map[string]interface{}{
"items": []interface{}{},
},
},
},
expectError: true,
errorContains: "unsupported dashboard format",
},
{
name: "invalid dashboard (no panels or elements) returns error",
dashboard: map[string]interface{}{
"title": "Invalid Dashboard",
"description": "This dashboard has no panels or elements",
"tags": []string{"test"},
},
expectError: true,
errorContains: "unsupported dashboard format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
queries, err := extractQueriesFromDashboard(tt.dashboard)
if tt.expectError {
require.Error(t, err, "Expected error but got none")
require.Contains(t, err.Error(), tt.errorContains, "Error message doesn't contain expected substring")
} else {
require.NoError(t, err, "Expected no error but got: %v", err)
require.NotNil(t, queries, "Queries should not be nil for valid dashboard")
}
})
}
}

View File

@@ -0,0 +1,173 @@
package validator
import (
"errors"
"fmt"
"net/http"
)
// ErrorCode represents the type of error that occurred
type ErrorCode string
const (
// Datasource-related errors
ErrCodeDatasourceNotFound ErrorCode = "datasource_not_found"
ErrCodeDatasourceWrongType ErrorCode = "datasource_wrong_type"
ErrCodeDatasourceUnreachable ErrorCode = "datasource_unreachable"
ErrCodeDatasourceAuth ErrorCode = "datasource_auth_failed"
ErrCodeDatasourceConfig ErrorCode = "datasource_config_error"
// API-related errors
ErrCodeAPIUnavailable ErrorCode = "api_unavailable"
ErrCodeAPIInvalidResponse ErrorCode = "api_invalid_response"
ErrCodeAPIRateLimit ErrorCode = "api_rate_limit"
ErrCodeAPITimeout ErrorCode = "api_timeout"
// Validation errors
ErrCodeInvalidDashboard ErrorCode = "invalid_dashboard"
ErrCodeUnsupportedDashVersion ErrorCode = "unsupported_dashboard_version"
ErrCodeInvalidQuery ErrorCode = "invalid_query"
// Internal errors
ErrCodeInternal ErrorCode = "internal_error"
)
// ValidationError represents a structured error with context
type ValidationError struct {
Code ErrorCode
Message string
Details map[string]interface{}
StatusCode int
Cause error
}
// Error implements the error interface
func (e *ValidationError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %s (caused by: %v)", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// Unwrap implements error unwrapping
func (e *ValidationError) Unwrap() error {
return e.Cause
}
// NewValidationError creates a new ValidationError
func NewValidationError(code ErrorCode, message string, statusCode int) *ValidationError {
return &ValidationError{
Code: code,
Message: message,
StatusCode: statusCode,
Details: make(map[string]interface{}),
}
}
// WithCause adds the underlying error cause
func (e *ValidationError) WithCause(err error) *ValidationError {
e.Cause = err
return e
}
// WithDetail adds contextual information
func (e *ValidationError) WithDetail(key string, value interface{}) *ValidationError {
e.Details[key] = value
return e
}
// Common error constructors
// NewDatasourceNotFoundError creates an error for datasource not found
func NewDatasourceNotFoundError(uid string, namespace string) *ValidationError {
return NewValidationError(
ErrCodeDatasourceNotFound,
fmt.Sprintf("datasource not found: %s", uid),
http.StatusNotFound,
).WithDetail("datasourceUID", uid).WithDetail("namespace", namespace)
}
// NewDatasourceWrongTypeError creates an error for wrong datasource type
func NewDatasourceWrongTypeError(uid string, expectedType string, actualType string) *ValidationError {
return NewValidationError(
ErrCodeDatasourceWrongType,
fmt.Sprintf("datasource %s has wrong type: expected %s, got %s", uid, expectedType, actualType),
http.StatusBadRequest,
).WithDetail("datasourceUID", uid).
WithDetail("expectedType", expectedType).
WithDetail("actualType", actualType)
}
// NewDatasourceUnreachableError creates an error for unreachable datasource
func NewDatasourceUnreachableError(uid string, url string, cause error) *ValidationError {
return NewValidationError(
ErrCodeDatasourceUnreachable,
fmt.Sprintf("datasource %s at %s is unreachable", uid, url),
http.StatusServiceUnavailable,
).WithDetail("datasourceUID", uid).
WithDetail("url", url).
WithCause(cause)
}
// NewAPIUnavailableError creates an error for unavailable API
func NewAPIUnavailableError(statusCode int, responseBody string, cause error) *ValidationError {
return NewValidationError(
ErrCodeAPIUnavailable,
fmt.Sprintf("Prometheus API returned status %d", statusCode),
http.StatusBadGateway,
).WithDetail("upstreamStatus", statusCode).
WithDetail("responseBody", responseBody).
WithCause(cause)
}
// NewAPIInvalidResponseError creates an error for invalid API response
func NewAPIInvalidResponseError(message string, cause error) *ValidationError {
return NewValidationError(
ErrCodeAPIInvalidResponse,
fmt.Sprintf("Prometheus API returned invalid response: %s", message),
http.StatusBadGateway,
).WithCause(cause)
}
// NewAPITimeoutError creates an error for API timeout
func NewAPITimeoutError(url string, cause error) *ValidationError {
return NewValidationError(
ErrCodeAPITimeout,
fmt.Sprintf("request to %s timed out", url),
http.StatusGatewayTimeout,
).WithDetail("url", url).
WithCause(cause)
}
// NewDatasourceAuthError creates an error for authentication failures
func NewDatasourceAuthError(uid string, statusCode int) *ValidationError {
return NewValidationError(
ErrCodeDatasourceAuth,
fmt.Sprintf("authentication failed for datasource %s (status %d)", uid, statusCode),
http.StatusUnauthorized,
).WithDetail("datasourceUID", uid).
WithDetail("upstreamStatus", statusCode)
}
// IsValidationError checks if an error is a ValidationError
func IsValidationError(err error) bool {
var validationErr *ValidationError
return errors.As(err, &validationErr)
}
// GetValidationError extracts a ValidationError from an error chain
func GetValidationError(err error) *ValidationError {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
return validationErr
}
return nil
}
// GetHTTPStatusCode returns the appropriate HTTP status code for an error
func GetHTTPStatusCode(err error) int {
if validationErr := GetValidationError(err); validationErr != nil {
return validationErr.StatusCode
}
return http.StatusInternalServerError
}

View File

@@ -0,0 +1,131 @@
package validator
import (
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewDatasourceNotFoundError(t *testing.T) {
err := NewDatasourceNotFoundError("test-uid", "org-1")
require.Equal(t, ErrCodeDatasourceNotFound, err.Code)
require.Equal(t, http.StatusNotFound, err.StatusCode)
require.Equal(t, "test-uid", err.Details["datasourceUID"])
require.Equal(t, "org-1", err.Details["namespace"])
}
func TestNewDatasourceWrongTypeError(t *testing.T) {
err := NewDatasourceWrongTypeError("test-uid", "prometheus", "influxdb")
require.Equal(t, ErrCodeDatasourceWrongType, err.Code)
require.Equal(t, http.StatusBadRequest, err.StatusCode)
require.Equal(t, "prometheus", err.Details["expectedType"])
require.Equal(t, "influxdb", err.Details["actualType"])
}
func TestNewDatasourceUnreachableError(t *testing.T) {
cause := errors.New("connection refused")
err := NewDatasourceUnreachableError("test-uid", "http://localhost:9090", cause)
require.Equal(t, ErrCodeDatasourceUnreachable, err.Code)
require.Equal(t, http.StatusServiceUnavailable, err.StatusCode)
require.Equal(t, cause, err.Cause)
require.Equal(t, "http://localhost:9090", err.Details["url"])
}
func TestNewAPIUnavailableError(t *testing.T) {
err := NewAPIUnavailableError(503, "service unavailable", nil)
require.Equal(t, ErrCodeAPIUnavailable, err.Code)
require.Equal(t, http.StatusBadGateway, err.StatusCode)
require.Equal(t, 503, err.Details["upstreamStatus"])
}
func TestNewAPIInvalidResponseError(t *testing.T) {
cause := errors.New("invalid JSON")
err := NewAPIInvalidResponseError("missing data field", cause)
require.Equal(t, ErrCodeAPIInvalidResponse, err.Code)
require.Equal(t, http.StatusBadGateway, err.StatusCode)
require.Equal(t, cause, err.Cause)
}
func TestNewAPITimeoutError(t *testing.T) {
cause := errors.New("context deadline exceeded")
err := NewAPITimeoutError("http://localhost:9090/api/v1/query", cause)
require.Equal(t, ErrCodeAPITimeout, err.Code)
require.Equal(t, http.StatusGatewayTimeout, err.StatusCode)
require.Equal(t, cause, err.Cause)
}
func TestNewDatasourceAuthError(t *testing.T) {
err := NewDatasourceAuthError("test-uid", 401)
require.Equal(t, ErrCodeDatasourceAuth, err.Code)
require.Equal(t, http.StatusUnauthorized, err.StatusCode)
require.Equal(t, 401, err.Details["upstreamStatus"])
}
func TestValidationErrorChaining(t *testing.T) {
cause := errors.New("network error")
err := NewValidationError(ErrCodeInternal, "test error", http.StatusInternalServerError).
WithCause(cause).
WithDetail("key1", "value1").
WithDetail("key2", 123)
require.Equal(t, cause, err.Cause)
require.Equal(t, "value1", err.Details["key1"])
require.Equal(t, 123, err.Details["key2"])
}
func TestIsValidationError(t *testing.T) {
validationErr := NewDatasourceNotFoundError("test-uid", "org-1")
regularErr := errors.New("regular error")
require.True(t, IsValidationError(validationErr), "expected IsValidationError to return true for ValidationError")
require.False(t, IsValidationError(regularErr), "expected IsValidationError to return false for regular error")
}
func TestGetValidationError(t *testing.T) {
validationErr := NewDatasourceNotFoundError("test-uid", "org-1")
regularErr := errors.New("regular error")
retrieved := GetValidationError(validationErr)
require.NotNil(t, retrieved, "expected GetValidationError to return the ValidationError")
require.Equal(t, ErrCodeDatasourceNotFound, retrieved.Code)
retrieved = GetValidationError(regularErr)
require.Nil(t, retrieved, "expected GetValidationError to return nil for regular error")
}
func TestGetHTTPStatusCode(t *testing.T) {
validationErr := NewDatasourceNotFoundError("test-uid", "org-1")
regularErr := errors.New("regular error")
require.Equal(t, http.StatusNotFound, GetHTTPStatusCode(validationErr))
require.Equal(t, http.StatusInternalServerError, GetHTTPStatusCode(regularErr), "expected default status code for regular error")
}
func TestErrorUnwrap(t *testing.T) {
cause := errors.New("underlying error")
err := NewDatasourceUnreachableError("test-uid", "http://localhost:9090", cause)
require.Equal(t, cause, errors.Unwrap(err), "expected Unwrap to return the cause")
}
func TestErrorErrorMethod(t *testing.T) {
// Test without cause
err1 := NewDatasourceNotFoundError("test-uid", "org-1")
require.NotEmpty(t, err1.Error(), "expected non-empty error message")
// Test with cause
cause := errors.New("underlying error")
err2 := NewDatasourceUnreachableError("test-uid", "http://localhost:9090", cause)
errMsg2 := err2.Error()
require.NotEmpty(t, errMsg2, "expected non-empty error message")
require.Contains(t, errMsg2, "underlying error", "error message should include cause")
}

View File

@@ -0,0 +1,142 @@
package prometheus
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"github.com/grafana/grafana/apps/dashvalidator/pkg/validator"
)
// Fetcher fetches available metrics from a Prometheus datasource
type Fetcher struct{}
// NewFetcher creates a new Prometheus metrics fetcher
func NewFetcher() *Fetcher {
return &Fetcher{}
}
// prometheusResponse represents the Prometheus API response structure
type prometheusResponse struct {
Status string `json:"status"`
Data []string `json:"data"`
Error string `json:"error,omitempty"`
}
// FetchMetrics queries Prometheus to get all available metric names
// It uses the /api/v1/label/__name__/values endpoint
// The provided HTTP client should have proper authentication configured
func (f *Fetcher) FetchMetrics(ctx context.Context, datasourceURL string, client *http.Client) ([]string, error) {
// Build the API URL
baseURL, err := url.Parse(datasourceURL)
if err != nil {
return nil, validator.NewValidationError(
validator.ErrCodeDatasourceConfig,
"invalid datasource URL",
http.StatusBadRequest,
).WithCause(err).WithDetail("url", datasourceURL)
}
// Append Prometheus API endpoint to base URL path using path.Join
// This correctly handles datasources with existing paths (e.g., /api/prom)
endpoint := "api/v1/label/__name__/values"
baseURL.Path = path.Join(baseURL.Path, endpoint)
// Create the request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL.String(), nil)
if err != nil {
return nil, validator.NewValidationError(
validator.ErrCodeInternal,
"failed to create HTTP request",
http.StatusInternalServerError,
).WithCause(err)
}
// Execute the request using the provided authenticated client
resp, err := client.Do(req)
if err != nil {
// Check if it's a timeout error
if errors.Is(err, context.DeadlineExceeded) || strings.Contains(err.Error(), "timeout") {
return nil, validator.NewAPITimeoutError(baseURL.String(), err)
}
// Network or connection error - datasource is unreachable
return nil, validator.NewDatasourceUnreachableError("", datasourceURL, err)
}
defer resp.Body.Close()
// Read response body for error reporting
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
body = []byte("<unable to read response body>")
}
// Check HTTP status code
switch resp.StatusCode {
case http.StatusOK:
// Success - continue to parse response
case http.StatusUnauthorized, http.StatusForbidden:
// Authentication or authorization failure
return nil, validator.NewDatasourceAuthError("", resp.StatusCode).
WithDetail("url", baseURL.String()).
WithDetail("responseBody", string(body))
case http.StatusNotFound:
// Endpoint not found - might not be a valid Prometheus instance
return nil, validator.NewAPIUnavailableError(
resp.StatusCode,
string(body),
fmt.Errorf("endpoint not found - this may not be a valid Prometheus datasource"),
).WithDetail("url", baseURL.String())
case http.StatusTooManyRequests:
// Rate limiting
return nil, validator.NewValidationError(
validator.ErrCodeAPIRateLimit,
"Prometheus API rate limit exceeded",
http.StatusTooManyRequests,
).WithDetail("url", baseURL.String()).WithDetail("responseBody", string(body))
case http.StatusServiceUnavailable, http.StatusBadGateway, http.StatusGatewayTimeout:
// Upstream service is down or unavailable
return nil, validator.NewAPIUnavailableError(resp.StatusCode, string(body), nil).
WithDetail("url", baseURL.String())
default:
// Other error status codes
return nil, validator.NewAPIUnavailableError(resp.StatusCode, string(body), nil).
WithDetail("url", baseURL.String())
}
// Parse the response JSON
var promResp prometheusResponse
if err := json.Unmarshal(body, &promResp); err != nil {
return nil, validator.NewAPIInvalidResponseError(
"response is not valid JSON",
err,
).WithDetail("url", baseURL.String()).WithDetail("responseBody", string(body))
}
// Check Prometheus API status field
if promResp.Status != "success" {
errorMsg := promResp.Error
if errorMsg == "" {
errorMsg = "unknown error"
}
return nil, validator.NewAPIInvalidResponseError(
fmt.Sprintf("Prometheus API returned error status: %s", errorMsg),
nil,
).WithDetail("url", baseURL.String()).WithDetail("prometheusError", errorMsg)
}
// Validate that we got data
if promResp.Data == nil {
return nil, validator.NewAPIInvalidResponseError(
"response missing 'data' field",
nil,
).WithDetail("url", baseURL.String()).WithDetail("responseBody", string(body))
}
return promResp.Data, nil
}

View File

@@ -0,0 +1,49 @@
package prometheus
import (
"fmt"
"github.com/prometheus/prometheus/promql/parser"
)
// Parser extracts metric names from PromQL queries
type Parser struct{}
// NewParser creates a new PromQL parser
func NewParser() *Parser {
return &Parser{}
}
// ExtractMetrics parses a PromQL query and extracts all metric names
// For example: "rate(http_requests_total[5m])" returns ["http_requests_total"]
func (p *Parser) ExtractMetrics(query string) ([]string, error) {
// Parse the PromQL expression
expr, err := parser.ParseExpr(query)
if err != nil {
return nil, fmt.Errorf("failed to parse PromQL query: %w", err)
}
// Extract metric names by walking the AST
metrics := make(map[string]bool) // Use map to deduplicate
parser.Inspect(expr, func(node parser.Node, _ []parser.Node) error {
// VectorSelector represents a metric selector like "up" or "up{job="foo"}"
if vs, ok := node.(*parser.VectorSelector); ok {
metrics[vs.Name] = true
}
// MatrixSelector represents range queries like "up[5m]"
if ms, ok := node.(*parser.MatrixSelector); ok {
if vs, ok := ms.VectorSelector.(*parser.VectorSelector); ok {
metrics[vs.Name] = true
}
}
return nil
})
// Convert map to slice
result := make([]string, 0, len(metrics))
for metric := range metrics {
result = append(result, metric)
}
return result, nil
}

View File

@@ -0,0 +1,137 @@
package prometheus
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestExtractMetrics(t *testing.T) {
parser := NewParser()
tests := []struct {
name string
query string
expected []string
expectError bool
errorContains string
}{
// Category 1: Basic Extraction (3 tests - covers AST node types)
{
name: "simple metric",
query: "up",
expected: []string{"up"},
},
{
name: "metric with labels",
query: `up{job="api"}`,
expected: []string{"up"},
},
{
name: "range selector",
query: "up[5m]",
expected: []string{"up"},
},
// Category 2: Function Composition (2 tests - nested complexity)
{
name: "single function",
query: "rate(http_requests_total[5m])",
expected: []string{"http_requests_total"},
},
{
name: "nested functions",
query: "sum(rate(requests[5m]))",
expected: []string{"requests"},
},
// Category 3: Binary Operations (2 tests - multiple metrics)
{
name: "two metrics",
query: "metric_a + metric_b",
expected: []string{"metric_a", "metric_b"},
},
{
name: "three metrics nested",
query: "(a + b) / c",
expected: []string{"a", "b", "c"},
},
// Category 4: Deduplication (1 test - critical behavior)
{
name: "duplicate metric",
query: "up + up",
expected: []string{"up"},
},
// Category 5: Edge Cases (2 tests - boundary behaviors)
{
name: "no metrics (literals only)",
query: "1 + 1",
expected: []string{},
},
{
name: "built-in function without metric",
query: "time()",
expected: []string{},
},
{
name: "comparison operator",
query: "a > 5",
expected: []string{"a"},
},
// Category 6: Real Dashboard Patterns (3 tests - production queries)
{
name: "binary op with function and labels",
query: `(time() - process_start_time_seconds{job="prometheus", instance=~"$node"})`,
expected: []string{"process_start_time_seconds"},
},
{
name: "rate with regex label matcher",
query: `rate(prometheus_local_storage_ingested_samples_total{instance=~"$node"}[5m])`,
expected: []string{"prometheus_local_storage_ingested_samples_total"},
},
{
name: "metric with negation and multiple labels",
query: `prometheus_target_interval_length_seconds{quantile!="0.01", quantile!="0.05", instance=~"$node"}`,
expected: []string{"prometheus_target_interval_length_seconds"},
},
// Category 7: Error Handling (2 tests - validation)
{
name: "empty string",
query: "",
expectError: true,
errorContains: "parse",
},
{
name: "malformed expression",
query: "{{invalid}}",
expectError: true,
errorContains: "parse",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parser.ExtractMetrics(tt.query)
// Check error expectation
if tt.expectError {
require.Error(t, err, "Expected error for query: %q", tt.query)
if tt.errorContains != "" {
require.ErrorContains(t, err, tt.errorContains,
"Error should contain %q for query: %q", tt.errorContains, tt.query)
}
return
}
require.NoError(t, err, "Unexpected error for query: %q", tt.query)
// Check result matches expected (order-independent for multiple metrics)
require.ElementsMatch(t, tt.expected, result,
"ExtractMetrics(%q) returned unexpected metrics", tt.query)
})
}
}

View File

@@ -0,0 +1,149 @@
package prometheus
import (
"context"
"fmt"
"github.com/grafana/grafana/apps/dashvalidator/pkg/validator"
)
// Register Prometheus validator on package import
func init() {
validator.RegisterValidator("prometheus", func() validator.DatasourceValidator {
return NewValidator()
})
}
// Validator implements validator.DatasourceValidator for Prometheus datasources
type Validator struct {
parser *Parser
fetcher *Fetcher
}
// NewValidator creates a new Prometheus validator
func NewValidator() validator.DatasourceValidator {
return &Validator{
parser: NewParser(),
fetcher: NewFetcher(),
}
}
// ValidateQueries validates Prometheus queries against the datasource
func (v *Validator) ValidateQueries(ctx context.Context, queries []validator.Query, datasource validator.Datasource) (*validator.ValidationResult, error) {
fmt.Printf("[DEBUG PROM] Starting validation for %d queries against datasource %s\n", len(queries), datasource.URL)
result := &validator.ValidationResult{
TotalQueries: len(queries),
QueryBreakdown: make([]validator.QueryResult, 0, len(queries)),
}
// Step 1: Parse all queries to extract metrics
allMetrics := make(map[string]bool) // Use map to deduplicate
queryMetrics := make(map[int][]string)
for i, query := range queries {
fmt.Printf("[DEBUG PROM] Parsing query %d: %s\n", i, query.QueryText)
metrics, err := v.parser.ExtractMetrics(query.QueryText)
if err != nil {
// If we can't parse the query, we still continue with others
// but we don't count this query as "checked"
fmt.Printf("[DEBUG PROM] Failed to parse query %d: %v\n", i, err)
continue
}
fmt.Printf("[DEBUG PROM] Extracted %d metrics from query %d: %v\n", len(metrics), i, metrics)
result.CheckedQueries++
queryMetrics[i] = metrics
// Add to global metrics set
for _, metric := range metrics {
allMetrics[metric] = true
}
}
// Convert map to slice for fetcher
metricsToCheck := make([]string, 0, len(allMetrics))
for metric := range allMetrics {
metricsToCheck = append(metricsToCheck, metric)
}
result.TotalMetrics = len(metricsToCheck)
fmt.Printf("[DEBUG PROM] Total metrics to check: %d - %v\n", len(metricsToCheck), metricsToCheck)
// Step 2: Fetch available metrics from Prometheus
fmt.Printf("[DEBUG PROM] Fetching available metrics from %s\n", datasource.URL)
availableMetrics, err := v.fetcher.FetchMetrics(ctx, datasource.URL, datasource.HTTPClient)
if err != nil {
fmt.Printf("[DEBUG PROM] Failed to fetch metrics: %v\n", err)
return nil, fmt.Errorf("failed to fetch metrics from Prometheus: %w", err)
}
fmt.Printf("[DEBUG PROM] Fetched %d available metrics from Prometheus\n", len(availableMetrics))
// Build a set for O(1) lookup
availableSet := make(map[string]bool)
for _, metric := range availableMetrics {
availableSet[metric] = true
}
// Step 3: Calculate compatibility
missingMetricsMap := make(map[string]bool)
for _, metric := range metricsToCheck {
if !availableSet[metric] {
missingMetricsMap[metric] = true
}
}
result.FoundMetrics = result.TotalMetrics - len(missingMetricsMap)
// Convert missing metrics map to slice
result.MissingMetrics = make([]string, 0, len(missingMetricsMap))
for metric := range missingMetricsMap {
result.MissingMetrics = append(result.MissingMetrics, metric)
}
// Step 4: Build per-query breakdown
for i, query := range queries {
metrics, ok := queryMetrics[i]
if !ok {
// Query wasn't parsed successfully, skip
continue
}
queryResult := validator.QueryResult{
PanelTitle: query.PanelTitle,
PanelID: query.PanelID,
QueryRefID: query.RefID,
TotalMetrics: len(metrics),
}
// Check which metrics from this query are missing
queryMissing := make([]string, 0)
for _, metric := range metrics {
if missingMetricsMap[metric] {
queryMissing = append(queryMissing, metric)
}
}
queryResult.MissingMetrics = queryMissing
queryResult.FoundMetrics = queryResult.TotalMetrics - len(queryMissing)
// Calculate query-level compatibility score
if queryResult.TotalMetrics > 0 {
queryResult.CompatibilityScore = float64(queryResult.FoundMetrics) / float64(queryResult.TotalMetrics)
} else {
queryResult.CompatibilityScore = 1.0 // No metrics = perfect compatibility
}
result.QueryBreakdown = append(result.QueryBreakdown, queryResult)
}
// Step 5: Calculate overall compatibility score
if result.TotalMetrics > 0 {
result.CompatibilityScore = float64(result.FoundMetrics) / float64(result.TotalMetrics)
} else {
result.CompatibilityScore = 1.0 // No metrics = perfect compatibility
}
fmt.Printf("[DEBUG PROM] Validation complete! Score: %.2f, Found: %d/%d metrics\n",
result.CompatibilityScore, result.FoundMetrics, result.TotalMetrics)
return result, nil
}

View File

@@ -0,0 +1,74 @@
package validator
import (
"context"
"fmt"
"net/http"
)
// DatasourceValidator validates dashboard queries against a datasource
// Implementations exist per datasource type (Prometheus, MySQL, etc.)
type DatasourceValidator interface {
// ValidateQueries checks if queries are compatible with the datasource
ValidateQueries(ctx context.Context, queries []Query, datasource Datasource) (*ValidationResult, error)
}
// Query represents a dashboard query to validate
type Query struct {
RefID string // Query reference ID (A, B, C, etc.)
QueryText string // The actual query text (PromQL, SQL, etc.)
PanelTitle string // Panel title for user-friendly reporting
PanelID int // Panel ID for reference
}
// Datasource contains connection information for a datasource
type Datasource struct {
UID string // Datasource UID from dashboard
Type string // Datasource type (prometheus, mysql, etc.)
Name string // Datasource name for reporting
URL string // Datasource URL for API calls
HTTPClient *http.Client // Authenticated HTTP client for making requests
}
// ValidationResult contains validation results for a datasource
type ValidationResult struct {
TotalQueries int // Total number of queries found
CheckedQueries int // Number of queries successfully checked
TotalMetrics int // Total metrics/entities referenced
FoundMetrics int // Metrics found in datasource
MissingMetrics []string // List of missing metrics
QueryBreakdown []QueryResult // Per-query results
CompatibilityScore float64 // Overall compatibility (0.0 - 1.0)
}
// QueryResult contains validation results for a single query
type QueryResult struct {
PanelTitle string // Panel title
PanelID int // Panel ID
QueryRefID string // Query reference ID
TotalMetrics int // Metrics in this query
FoundMetrics int // Metrics found
MissingMetrics []string // Missing metrics for this query
CompatibilityScore float64 // Query compatibility (0.0 - 1.0)
}
// validatorRegistry holds registered validator constructors
// Validators register themselves using RegisterValidator in their init() functions
var validatorRegistry = make(map[string]func() DatasourceValidator)
// RegisterValidator registers a validator constructor for a datasource type
// This is called by validator implementations in their init() functions
// Example: validator.RegisterValidator("prometheus", func() validator.DatasourceValidator { return NewValidator() })
func RegisterValidator(dsType string, constructor func() DatasourceValidator) {
validatorRegistry[dsType] = constructor
}
// GetValidator returns a validator for the given datasource type
// Returns an error if the datasource type is not supported
func GetValidator(dsType string) (DatasourceValidator, error) {
constructor, ok := validatorRegistry[dsType]
if !ok {
return nil, fmt.Errorf("unsupported datasource type: %s", dsType)
}
return constructor(), nil
}

View File

@@ -0,0 +1,164 @@
package validator
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIsVariableReference(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{"dollar brace", "${prometheus}", true},
{"dollar simple", "$datasource", true},
{"double bracket", "[[prometheus]]", true},
{"concrete uid", "abcd1234", false},
{"empty string", "", false},
{"dollar only", "$", false},
{"empty braces", "${}", false},
{"number start", "$123", true}, // Changed: Grafana ACCEPTS digits (per \w+ regex)
{"all digits", "$999", true}, // New: All digits are valid per \w+
{"special chars dash", "$ds-name", false}, // Changed: Grafana REJECTS dashes (not in \w)
{"underscore", "$DS_PROMETHEUS", true},
{"complex variable", "${DS_PROMETHEUS}", true},
{"simple letter", "$p", true},
{"with fieldpath", "${var.field}", true}, // New: Test fieldPath syntax
{"with format", "[[var:text]]", true}, // New: Test format syntax
{"brace with format", "${var:json}", true}, // New: Test brace format syntax
{"digit in brackets", "[[123]]", true}, // New: Digits allowed in all patterns
{"empty brackets", "[[]]", false}, // New: Empty brackets rejected
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isVariableReference(tt.input)
require.Equal(t, tt.expected, result, "isVariableReference(%q) returned unexpected result", tt.input)
})
}
}
func TestExtractVariableName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"dollar brace", "${prometheus}", "prometheus"},
{"dollar simple", "$datasource", "datasource"},
{"double bracket", "[[prometheus]]", "prometheus"},
{"not variable", "concrete-uid", ""},
{"empty", "", ""},
{"complex name", "${DS_PROMETHEUS}", "DS_PROMETHEUS"},
{"with underscore", "$DS_NAME", "DS_NAME"},
{"digit variable", "$123", "123"}, // New: Digits are valid
{"with fieldpath", "${var.field}", "var"}, // Changed: Extract only name, not fieldPath
{"with format brace", "${var:json}", "var"}, // Changed: Extract only name, not format
{"with format bracket", "[[var:text]]", "var"}, // Changed: Extract only name, not format
{"fieldpath and format", "${var.field:json}", "var"}, // New: Extract only name from complex syntax
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractVariableName(tt.input)
require.Equal(t, tt.expected, result, "extractVariableName(%q) returned unexpected result", tt.input)
})
}
}
func TestIsPrometheusVariable(t *testing.T) {
// Dashboard with Prometheus __inputs
dashboardWithPrometheus := map[string]interface{}{
"__inputs": []interface{}{
map[string]interface{}{
"name": "DS_PROMETHEUS",
"type": "datasource",
"pluginId": "prometheus",
},
},
}
// Dashboard with MySQL __inputs
dashboardWithMySQL := map[string]interface{}{
"__inputs": []interface{}{
map[string]interface{}{
"name": "DS_MYSQL",
"type": "datasource",
"pluginId": "mysql",
},
},
}
// Dashboard without __inputs
dashboardWithoutInputs := map[string]interface{}{
"title": "Test Dashboard",
}
tests := []struct {
name string
varRef string
dashboard map[string]interface{}
expected bool
}{
{"prometheus variable with inputs", "${DS_PROMETHEUS}", dashboardWithPrometheus, true},
{"prometheus simple var", "$DS_PROMETHEUS", dashboardWithPrometheus, true},
{"mysql variable", "${DS_MYSQL}", dashboardWithMySQL, false},
{"not variable", "concrete-uid", dashboardWithPrometheus, false},
{"variable without inputs", "${prometheus}", dashboardWithoutInputs, true}, // Fallback to true for MVP
{"wrong variable name", "${OTHER}", dashboardWithPrometheus, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isPrometheusVariable(tt.varRef, tt.dashboard)
require.Equal(t, tt.expected, result, "isPrometheusVariable(%q, dashboard) returned unexpected result", tt.varRef)
})
}
}
func TestResolveDatasourceUID(t *testing.T) {
singleUID := "prom-uid-123"
dashboardWithPrometheus := map[string]interface{}{
"__inputs": []interface{}{
map[string]interface{}{
"name": "DS_PROMETHEUS",
"type": "datasource",
"pluginId": "prometheus",
},
},
}
dashboardWithMySQL := map[string]interface{}{
"__inputs": []interface{}{
map[string]interface{}{
"name": "DS_MYSQL",
"type": "datasource",
"pluginId": "mysql",
},
},
}
tests := []struct {
name string
uid string
dashboard map[string]interface{}
expectedUID string
description string
}{
{"concrete uid", "concrete-123", dashboardWithPrometheus, "concrete-123", "should return concrete UID as-is"},
{"prometheus variable", "${DS_PROMETHEUS}", dashboardWithPrometheus, singleUID, "should resolve to single datasource UID"},
{"prometheus simple var", "$DS_PROMETHEUS", dashboardWithPrometheus, singleUID, "should resolve simple $ syntax"},
{"mysql variable", "${DS_MYSQL}", dashboardWithMySQL, "${DS_MYSQL}", "should return non-Prometheus variable as-is"},
{"empty uid", "", dashboardWithPrometheus, "", "should return empty string as-is"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := resolveDatasourceUID(tt.uid, singleUID, tt.dashboard)
require.Equal(t, tt.expectedUID, result, "resolveDatasourceUID(%q, %q, dashboard): %s", tt.uid, singleUID, tt.description)
})
}
}

View File

@@ -40,7 +40,7 @@ export interface ManagedFieldsEntry {
subresource?: string;
}
export interface Investigation {
export interface DashboardCompatibilityScore {
kind: string;
apiVersion: string;
metadata: Metadata;

View File

@@ -0,0 +1,42 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// DataSourceMapping specifies a datasource to validate dashboard queries against.
// Maps logical datasource references in the dashboard to actual datasource instances.
export interface DataSourceMapping {
// Unique identifier of the datasource instance.
// Example: "prometheus-prod-us-west"
uid: string;
// Type of datasource plugin.
// MVP: Only "prometheus" supported.
// Future: "mysql", "postgres", "elasticsearch", etc.
type: string;
// Optional human-readable name for display in results.
// If not provided, UID will be used in error messages.
// Example: "Production Prometheus (US-West)"
name?: string;
}
export const defaultDataSourceMapping = (): DataSourceMapping => ({
uid: "",
type: "",
});
export interface Spec {
// Complete dashboard JSON object to validate.
// Must be a v1 dashboard schema (contains "panels" array).
// v2 dashboards (with "elements" structure) are not yet supported.
dashboardJson: Record<string, any>;
// Array of datasources to validate against.
// The validator will check dashboard queries against each datasource
// and provide per-datasource compatibility results.
//
// MVP: Only single datasource supported (array length = 1), Prometheus type only.
// Future: Will support multiple datasources for dashboards with mixed queries.
datasourceMappings: DataSourceMapping[];
}
export const defaultSpec = (): Spec => ({
dashboardJson: {},
datasourceMappings: [],
});

View File

@@ -0,0 +1,142 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// DataSourceResult contains validation results for a single datasource.
// Provides aggregate statistics and per-query breakdown of compatibility.
export interface DataSourceResult {
// Datasource UID that was validated (matches DataSourceMapping.uid)
uid: string;
// Datasource type (matches DataSourceMapping.type)
type: string;
// Optional display name (matches DataSourceMapping.name if provided)
name?: string;
// Total number of queries in the dashboard targeting this datasource.
// Includes all panel targets/queries that reference this datasource.
totalQueries: number;
// Number of queries successfully validated.
// May be less than totalQueries if some queries couldn't be parsed.
checkedQueries: number;
// Total number of unique metrics/identifiers referenced across all queries.
// For Prometheus: metric names extracted from PromQL expressions.
// For SQL datasources: table and column names.
totalMetrics: number;
// Number of metrics that exist in the datasource schema.
// foundMetrics <= totalMetrics
foundMetrics: number;
// Array of metric names that were referenced but don't exist.
// Useful for debugging why a dashboard shows "no data".
// Example for Prometheus: ["http_requests_total", "api_latency_seconds"]
missingMetrics: string[];
// Per-query breakdown showing which specific queries have issues.
// One entry per query target (refId: "A", "B", "C", etc.) in each panel.
// Allows pinpointing exactly which panel/query needs fixing.
queryBreakdown: QueryBreakdown[];
// Overall compatibility score for this datasource (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// Used to calculate the global compatibilityScore in status.
compatibilityScore: number;
}
export const defaultDataSourceResult = (): DataSourceResult => ({
uid: "",
type: "",
totalQueries: 0,
checkedQueries: 0,
totalMetrics: 0,
foundMetrics: 0,
missingMetrics: [],
queryBreakdown: [],
compatibilityScore: 0,
});
// QueryBreakdown provides compatibility details for a single query within a panel.
// Granular per-query results allow users to identify exactly which queries need fixing.
//
// Note: A panel can have multiple queries (refId: "A", "B", "C", etc.),
// so there may be multiple QueryBreakdown entries for the same panelID.
export interface QueryBreakdown {
// Human-readable panel title for context.
// Example: "CPU Usage", "Request Rate"
panelTitle: string;
// Numeric panel ID from dashboard JSON.
// Used to correlate with dashboard structure.
panelID: number;
// Query identifier within the panel.
// Values: "A", "B", "C", etc. (from panel.targets[].refId)
// Uniquely identifies which query in a multi-query panel this refers to.
queryRefId: string;
// Number of unique metrics referenced in this specific query.
// For Prometheus: metrics extracted from the PromQL expr.
// Example: rate(http_requests_total[5m]) references 1 metric.
totalMetrics: number;
// Number of those metrics that exist in the datasource.
// foundMetrics <= totalMetrics
foundMetrics: number;
// Array of missing metric names specific to this query.
// Helps identify exactly which part of a query expression will fail.
// Empty array means query is fully compatible.
missingMetrics: string[];
// Compatibility percentage for this individual query (0-100).
// Calculated as: (foundMetrics / totalMetrics) * 100
// 100 = query will work perfectly, 0 = query will return no data.
compatibilityScore: number;
}
export const defaultQueryBreakdown = (): QueryBreakdown => ({
panelTitle: "",
panelID: 0,
queryRefId: "",
totalMetrics: 0,
foundMetrics: 0,
missingMetrics: [],
compatibilityScore: 0,
});
export interface OperatorState {
// lastEvaluation is the ResourceVersion last evaluated
lastEvaluation: string;
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
state: "success" | "in_progress" | "failed";
// descriptiveState is an optional more descriptive state field which has no requirements on format
descriptiveState?: string;
// details contains any extra information that is operator-specific
details?: Record<string, any>;
}
export const defaultOperatorState = (): OperatorState => ({
lastEvaluation: "",
state: "success",
});
export interface Status {
// Overall compatibility score across all datasources (0-100).
// Calculated as: (total found metrics / total referenced metrics) * 100
//
// Score interpretation:
// - 100: Perfect compatibility, all queries will work
// - 80-99: Excellent, minor missing metrics
// - 50-79: Fair, significant missing metrics
// - 0-49: Poor, most queries will fail
compatibilityScore: number;
// Per-datasource validation results.
// Array length matches spec.datasourceMappings.
// Each element contains detailed metrics and query-level breakdown.
datasourceResults: DataSourceResult[];
// ISO 8601 timestamp of when validation was last performed.
// Example: "2024-01-15T10:30:00Z"
lastChecked?: string;
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
operatorStates?: Record<string, OperatorState>;
// Human-readable summary of validation result.
// Examples: "All queries compatible", "3 missing metrics found"
message?: string;
// additionalFields is reserved for future use
additionalFields?: Record<string, any>;
}
export const defaultStatus = (): Status => ({
compatibilityScore: 0,
datasourceResults: [],
});

View File

@@ -24,8 +24,6 @@ replace github.com/grafana/grafana/apps/alerting/historian => ../alerting/histor
replace github.com/grafana/grafana/apps/correlations => ../correlations
replace github.com/grafana/grafana/apps/investigations => ../investigations
replace github.com/grafana/grafana/apps/logsdrilldown => ../logsdrilldown
replace github.com/grafana/grafana/apps/playlist => ../playlist

View File

@@ -1,43 +0,0 @@
package v0alpha1
import (
"fmt"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
func (u User) AuthID() string {
meta, err := utils.MetaAccessor(&u)
if err != nil {
return ""
}
// TODO: Workaround until we move all definitions
// After having all resource definitions here in the app, we can remove this
// and we need to change the List authorization to use the MetaAccessor and the GetDeprecatedInternalID method
//nolint:staticcheck
return fmt.Sprintf("%d", meta.GetDeprecatedInternalID())
}
func (s ServiceAccount) AuthID() string {
meta, err := utils.MetaAccessor(&s)
if err != nil {
return ""
}
// TODO: Workaround until we move all definitions
// After having all resource definitions here in the app, we can remove this
// and we need to change the List authorization to use the MetaAccessor and the GetDeprecatedInternalID method
//nolint:staticcheck
return fmt.Sprintf("%d", meta.GetDeprecatedInternalID())
}
func (t Team) AuthID() string {
meta, err := utils.MetaAccessor(&t)
if err != nil {
return ""
}
// TODO: Workaround until we move all definitions
// After having all resource definitions here in the app, we can remove this
// and we need to change the List authorization to use the MetaAccessor and the GetDeprecatedInternalID method
//nolint:staticcheck
return fmt.Sprintf("%d", meta.GetDeprecatedInternalID())
}

View File

@@ -1,152 +0,0 @@
[
{
"id": "896312ce-65b0-4b50-ade1-e7f04fa22c66",
"title": "Thursday morning investigation",
"hasCustomName": false,
"isFavorite": false,
"collectables": [
{
"origin": "Explore Logs",
"type": "timeseries",
"queries": [
{
"refId": "LABEL_BREAKDOWN_VALUES",
"queryType": "range",
"editorMode": "code",
"supportingQueryType": "grafana-lokiexplore-app",
"legendFormat": "{{detected_level}}",
"expr": "sum(count_over_time({service_name=\"web_app_1\"} | detected_level != \"\"[$__auto])) by (detected_level)"
}
],
"timeRange": {
"to": "2025-02-13T11:31:20.536Z",
"from": "2025-02-13T11:16:20.536Z",
"raw": {
"from": "now-15m",
"to": "now"
}
},
"datasource": {
"uid": "fe9k7u07b1a0wc"
},
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
"id": "LABEL_BREAKDOWN_VALUES_detected_level",
"title": "detected_level",
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
"createdAt": "2025-02-13T11:31:23.637Z"
}
],
"createdAt": "2025-02-13T11:31:23.636Z",
"updatedAt": "2025-02-13T11:31:23.637Z",
"viewMode": {
"mode": "compact",
"showComments": true,
"showTooltips": false
}
},
{
"id": "e9cf1958-d0ed-46b7-b597-9052c7648656",
"title": "Thursday morning investigation",
"hasCustomName": false,
"isFavorite": false,
"collectables": [
{
"origin": "Explore Logs",
"type": "timeseries",
"queries": [
{
"refId": "LABEL_BREAKDOWN_VALUES",
"queryType": "range",
"editorMode": "code",
"supportingQueryType": "grafana-lokiexplore-app",
"legendFormat": "{{detected_level}}",
"expr": "sum(count_over_time({service_name=\"web_app_1\"} | detected_level != \"\"[$__auto])) by (detected_level)"
}
],
"timeRange": {
"to": "2025-02-13T11:31:20.536Z",
"from": "2025-02-13T11:16:20.536Z",
"raw": {
"from": "now-15m",
"to": "now"
}
},
"datasource": {
"uid": "fe9k7u07b1a0wc"
},
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
"id": "LABEL_BREAKDOWN_VALUES_detected_level",
"title": "detected_level",
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
"createdAt": "2025-02-13T11:31:23.638Z"
},
{
"origin": "Explore Logs",
"type": "timeseries",
"queries": [
{
"refId": "LABEL_BREAKDOWN_VALUES",
"queryType": "range",
"editorMode": "code",
"supportingQueryType": "grafana-lokiexplore-app",
"legendFormat": "{{service_name}}",
"expr": "sum(count_over_time({service_name=\"web_app_1\",service_name != \"\"} [$__auto])) by (service_name)"
}
],
"timeRange": {
"to": "2025-02-13T11:31:20.536Z",
"from": "2025-02-13T11:16:20.536Z",
"raw": {
"from": "now-15m",
"to": "now"
}
},
"datasource": {
"uid": "fe9k7u07b1a0wc"
},
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
"id": "LABEL_BREAKDOWN_VALUES_service_name",
"title": "service_name",
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
"createdAt": "2025-02-13T11:31:41.507Z"
},
{
"origin": "Explore Logs",
"type": "timeseries",
"queries": [
{
"refId": "LABEL_BREAKDOWN_VALUES",
"queryType": "range",
"editorMode": "code",
"supportingQueryType": "grafana-lokiexplore-app",
"legendFormat": "{{service}}",
"expr": "sum(count_over_time({service_name=\"web_app_1\",service != \"\"} [$__auto])) by (service)"
}
],
"timeRange": {
"to": "2025-02-13T11:31:20.536Z",
"from": "2025-02-13T11:16:20.536Z",
"raw": {
"from": "now-15m",
"to": "now"
}
},
"datasource": {
"uid": "fe9k7u07b1a0wc"
},
"url": "http://localhost:3000/a/grafana-lokiexplore-app/explore/service/web_app_1/labels?patterns=%5B%5D&from=now-15m&to=now&var-ds=fe9k7u07b1a0wc&var-filters=service_name%7C%3D%7Cweb_app_1&var-fields=&var-levels=&var-metadata=&var-patterns=&var-lineFilterV2=&var-lineFilters=&urlColumns=%5B%5D&visualizationType=%22logs%22&displayedFields=%5B%5D&timezone=browser&var-all-fields=&var-labelBy=$__all",
"id": "LABEL_BREAKDOWN_VALUES_service",
"title": "service",
"logoPath": "public/plugins/grafana-lokiexplore-app/img/img/logo.svg",
"createdAt": "2025-02-13T11:31:43.698Z"
}
],
"createdAt": "2025-02-13T11:31:23.637Z",
"updatedAt": "2025-02-13T11:31:43.698Z",
"viewMode": {
"mode": "compact",
"showComments": true,
"showTooltips": false
}
}
]

View File

@@ -1,102 +0,0 @@
module github.com/grafana/grafana/apps/investigations
go 1.25.5
require (
github.com/grafana/grafana-app-sdk v0.48.7
k8s.io/apimachinery v0.34.3
k8s.io/klog/v2 v2.130.1
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // 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/evanphx/json-patch v5.9.11+incompatible // 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-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // 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.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/grafana-app-sdk/logging v0.48.7 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // 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.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/gomega v1.36.2 // 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.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.3 // indirect
k8s.io/apiextensions-apiserver v0.34.3 // indirect
k8s.io/client-go v0.34.3 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

View File

@@ -1,264 +0,0 @@
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/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/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/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
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.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/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/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
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.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
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.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
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=
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.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.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g=
k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
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-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/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=

View File

@@ -1,43 +0,0 @@
package investigations
// Collectable represents an item collected during investigation
#Collectable: {
id: string
createdAt: string
title: string
origin: string
type: string
queries: [...string] // +listType=atomic
timeRange: #TimeRange
datasource: #DatasourceRef
url: string
logoPath?: string
note: string
noteUpdatedAt: string
fieldConfig: string
}
#CollectableSummary: {
id: string
title: string
logoPath: string
origin: string
}
// TimeRange represents a time range with both absolute and relative values
#TimeRange: {
from: string
to: string
raw: {
from: string
to: string
}
}
// DatasourceRef is a reference to a datasource
#DatasourceRef: {
uid: string
}

View File

@@ -1,4 +0,0 @@
module: "github.com/grafana/grafana/apps/investigations"
language: {
version: "v0.11.0"
}

View File

@@ -1,43 +0,0 @@
package investigations
investigationV0alpha1: {
kind: "Investigation"
pluralName: "Investigations"
schema: {
spec: {
title: string
createdByProfile: #Person
hasCustomName: bool
isFavorite: bool
overviewNote: string
overviewNoteUpdatedAt: string
collectables: [...#Collectable] // +listType=atomic
viewMode: #ViewMode
}
}
}
// Type definition for investigation summaries
#InvestigationSummary: {
title: string
createdByProfile: #Person
hasCustomName: bool
isFavorite: bool
overviewNote: string
overviewNoteUpdatedAt: string
viewMode: #ViewMode
collectableSummaries: [...#CollectableSummary] // +listType=atomic
}
// Person represents a user profile with basic information
#Person: {
uid: string // Unique identifier for the user
name: string // Display name of the user
gravatarUrl: string // URL to user's Gravatar image
}
#ViewMode: {
mode: "compact" | "full"
showComments: bool
showTooltips: bool
}

View File

@@ -1,18 +0,0 @@
package investigations
investigationIndexV0alpha1:{
kind: "InvestigationIndex"
pluralName: "InvestigationIndexes"
schema: {
spec: {
// Title of the index, e.g. 'Favorites' or 'My Investigations'
title: string
// The Person who owns this investigation index
owner: #Person
// Array of investigation summaries
investigationSummaries: [...#InvestigationSummary] // +listType=atomic
}
}
}

View File

@@ -1,18 +0,0 @@
package investigations
manifest: {
appName: "investigations"
groupOverride: "investigations.grafana.app"
versions: {
"v0alpha1": {
codegen: {
ts: {enabled: false}
go: {enabled: true}
}
kinds: [
investigationV0alpha1,
investigationIndexV0alpha1,
]
}
}
}

View File

@@ -1,18 +0,0 @@
package v0alpha1
import "k8s.io/apimachinery/pkg/runtime/schema"
const (
// APIGroup is the API group used by all kinds in this package
APIGroup = "investigations.grafana.app"
// APIVersion is the API version used by all kinds in this package
APIVersion = "v0alpha1"
)
var (
// GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package
GroupVersion = schema.GroupVersion{
Group: APIGroup,
Version: APIVersion,
}
)

View File

@@ -1,80 +0,0 @@
package v0alpha1
import (
"context"
"github.com/grafana/grafana-app-sdk/resource"
)
type InvestigationClient struct {
client *resource.TypedClient[*Investigation, *InvestigationList]
}
func NewInvestigationClient(client resource.Client) *InvestigationClient {
return &InvestigationClient{
client: resource.NewTypedClient[*Investigation, *InvestigationList](client, InvestigationKind()),
}
}
func NewInvestigationClientFromGenerator(generator resource.ClientGenerator) (*InvestigationClient, error) {
c, err := generator.ClientFor(InvestigationKind())
if err != nil {
return nil, err
}
return NewInvestigationClient(c), nil
}
func (c *InvestigationClient) Get(ctx context.Context, identifier resource.Identifier) (*Investigation, error) {
return c.client.Get(ctx, identifier)
}
func (c *InvestigationClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationList, error) {
return c.client.List(ctx, namespace, opts)
}
func (c *InvestigationClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationList, error) {
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
for resp.GetContinue() != "" {
page, err := c.client.List(ctx, namespace, resource.ListOptions{
Continue: resp.GetContinue(),
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
resp.SetContinue(page.GetContinue())
resp.SetResourceVersion(page.GetResourceVersion())
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
}
return resp, nil
}
func (c *InvestigationClient) Create(ctx context.Context, obj *Investigation, opts resource.CreateOptions) (*Investigation, error) {
// Make sure apiVersion and kind are set
obj.APIVersion = GroupVersion.Identifier()
obj.Kind = InvestigationKind().Kind()
return c.client.Create(ctx, obj, opts)
}
func (c *InvestigationClient) Update(ctx context.Context, obj *Investigation, opts resource.UpdateOptions) (*Investigation, error) {
return c.client.Update(ctx, obj, opts)
}
func (c *InvestigationClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Investigation, error) {
return c.client.Patch(ctx, identifier, req, opts)
}
func (c *InvestigationClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
return c.client.Delete(ctx, identifier, opts)
}

View File

@@ -1,28 +0,0 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"encoding/json"
"io"
"github.com/grafana/grafana-app-sdk/resource"
)
// InvestigationJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type InvestigationJSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*InvestigationJSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*InvestigationJSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &InvestigationJSONCodec{}

View File

@@ -1,31 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
time "time"
)
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type InvestigationMetadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
ResourceVersion string `json:"resourceVersion"`
Generation int64 `json:"generation"`
UpdatedBy string `json:"updatedBy"`
Labels map[string]string `json:"labels"`
}
// NewInvestigationMetadata creates a new InvestigationMetadata object.
func NewInvestigationMetadata() *InvestigationMetadata {
return &InvestigationMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}

View File

@@ -1,293 +0,0 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"time"
)
// +k8s:openapi-gen=true
type Investigation struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the Investigation
Spec InvestigationSpec `json:"spec" yaml:"spec"`
}
func (o *Investigation) GetSpec() any {
return o.Spec
}
func (o *Investigation) SetSpec(spec any) error {
cast, ok := spec.(InvestigationSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *Investigation) GetSubresources() map[string]any {
return map[string]any{}
}
func (o *Investigation) GetSubresource(name string) (any, bool) {
switch name {
default:
return nil, false
}
}
func (o *Investigation) SetSubresource(name string, value any) error {
switch name {
default:
return fmt.Errorf("subresource '%s' does not exist", name)
}
}
func (o *Investigation) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
Namespace: o.ObjectMeta.Namespace,
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
func (o *Investigation) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
Group: metadata.Group,
Version: metadata.Version,
Kind: metadata.Kind,
})
}
func (o *Investigation) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
deletionTimestamp = &dt.Time
}
// Legacy ExtraFields support
extraFields := make(map[string]any)
if o.Annotations != nil {
extraFields["annotations"] = o.Annotations
}
if o.ManagedFields != nil {
extraFields["managedFields"] = o.ManagedFields
}
if o.OwnerReferences != nil {
extraFields["ownerReferences"] = o.OwnerReferences
}
return resource.CommonMetadata{
UID: string(o.UID),
ResourceVersion: o.ResourceVersion,
Generation: o.Generation,
Labels: o.Labels,
CreationTimestamp: o.CreationTimestamp.Time,
DeletionTimestamp: deletionTimestamp,
Finalizers: o.Finalizers,
UpdateTimestamp: o.GetUpdateTimestamp(),
CreatedBy: o.GetCreatedBy(),
UpdatedBy: o.GetUpdatedBy(),
ExtraFields: extraFields,
}
}
func (o *Investigation) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
o.Labels = metadata.Labels
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
if metadata.DeletionTimestamp != nil {
dt := metav1.NewTime(*metadata.DeletionTimestamp)
o.DeletionTimestamp = &dt
} else {
o.DeletionTimestamp = nil
}
o.Finalizers = metadata.Finalizers
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
if !metadata.UpdateTimestamp.IsZero() {
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
}
if metadata.CreatedBy != "" {
o.SetCreatedBy(metadata.CreatedBy)
}
if metadata.UpdatedBy != "" {
o.SetUpdatedBy(metadata.UpdatedBy)
}
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
if metadata.ExtraFields != nil {
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
if cast, ok := annotations.(map[string]string); ok {
o.Annotations = cast
}
}
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
o.ManagedFields = cast
}
}
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
o.OwnerReferences = cast
}
}
}
}
func (o *Investigation) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *Investigation) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *Investigation) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *Investigation) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *Investigation) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *Investigation) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *Investigation) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *Investigation) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *Investigation) DeepCopy() *Investigation {
cpy := &Investigation{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *Investigation) DeepCopyInto(dst *Investigation) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
}
// Interface compliance compile-time check
var _ resource.Object = &Investigation{}
// +k8s:openapi-gen=true
type InvestigationList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []Investigation `json:"items" yaml:"items"`
}
func (o *InvestigationList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *InvestigationList) Copy() resource.ListObject {
cpy := &InvestigationList{
TypeMeta: o.TypeMeta,
Items: make([]Investigation, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*Investigation); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *InvestigationList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
}
return items
}
func (o *InvestigationList) SetItems(items []resource.Object) {
o.Items = make([]Investigation, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*Investigation)
}
}
func (o *InvestigationList) DeepCopy() *InvestigationList {
cpy := &InvestigationList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *InvestigationList) DeepCopyInto(dst *InvestigationList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &InvestigationList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *InvestigationSpec) DeepCopy() *InvestigationSpec {
cpy := &InvestigationSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *InvestigationSpec) DeepCopyInto(dst *InvestigationSpec) {
resource.CopyObjectInto(dst, s)
}

View File

@@ -1,34 +0,0 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaInvestigation = resource.NewSimpleSchema("investigations.grafana.app", "v0alpha1", &Investigation{}, &InvestigationList{}, resource.WithKind("Investigation"),
resource.WithPlural("investigations"), resource.WithScope(resource.NamespacedScope))
kindInvestigation = resource.Kind{
Schema: schemaInvestigation,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &InvestigationJSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func InvestigationKind() resource.Kind {
return kindInvestigation
}
// Schema returns a resource.SimpleSchema representation of Investigation
func InvestigationSchema() *resource.SimpleSchema {
return schemaInvestigation
}
// Interface compliance checks
var _ resource.Schema = kindInvestigation

View File

@@ -1,126 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// Person represents a user profile with basic information
// +k8s:openapi-gen=true
type InvestigationPerson struct {
// Unique identifier for the user
Uid string `json:"uid"`
// Display name of the user
Name string `json:"name"`
// URL to user's Gravatar image
GravatarUrl string `json:"gravatarUrl"`
}
// NewInvestigationPerson creates a new InvestigationPerson object.
func NewInvestigationPerson() *InvestigationPerson {
return &InvestigationPerson{}
}
// Collectable represents an item collected during investigation
// +k8s:openapi-gen=true
type InvestigationCollectable struct {
Id string `json:"id"`
CreatedAt string `json:"createdAt"`
Title string `json:"title"`
Origin string `json:"origin"`
Type string `json:"type"`
// +listType=atomic
Queries []string `json:"queries"`
TimeRange InvestigationTimeRange `json:"timeRange"`
Datasource InvestigationDatasourceRef `json:"datasource"`
Url string `json:"url"`
LogoPath *string `json:"logoPath,omitempty"`
Note string `json:"note"`
NoteUpdatedAt string `json:"noteUpdatedAt"`
FieldConfig string `json:"fieldConfig"`
}
// NewInvestigationCollectable creates a new InvestigationCollectable object.
func NewInvestigationCollectable() *InvestigationCollectable {
return &InvestigationCollectable{
Queries: []string{},
TimeRange: *NewInvestigationTimeRange(),
Datasource: *NewInvestigationDatasourceRef(),
}
}
// TimeRange represents a time range with both absolute and relative values
// +k8s:openapi-gen=true
type InvestigationTimeRange struct {
From string `json:"from"`
To string `json:"to"`
Raw InvestigationV0alpha1TimeRangeRaw `json:"raw"`
}
// NewInvestigationTimeRange creates a new InvestigationTimeRange object.
func NewInvestigationTimeRange() *InvestigationTimeRange {
return &InvestigationTimeRange{
Raw: *NewInvestigationV0alpha1TimeRangeRaw(),
}
}
// DatasourceRef is a reference to a datasource
// +k8s:openapi-gen=true
type InvestigationDatasourceRef struct {
Uid string `json:"uid"`
}
// NewInvestigationDatasourceRef creates a new InvestigationDatasourceRef object.
func NewInvestigationDatasourceRef() *InvestigationDatasourceRef {
return &InvestigationDatasourceRef{}
}
// +k8s:openapi-gen=true
type InvestigationViewMode struct {
Mode InvestigationViewModeMode `json:"mode"`
ShowComments bool `json:"showComments"`
ShowTooltips bool `json:"showTooltips"`
}
// NewInvestigationViewMode creates a new InvestigationViewMode object.
func NewInvestigationViewMode() *InvestigationViewMode {
return &InvestigationViewMode{}
}
// +k8s:openapi-gen=true
type InvestigationSpec struct {
Title string `json:"title"`
CreatedByProfile InvestigationPerson `json:"createdByProfile"`
HasCustomName bool `json:"hasCustomName"`
IsFavorite bool `json:"isFavorite"`
OverviewNote string `json:"overviewNote"`
OverviewNoteUpdatedAt string `json:"overviewNoteUpdatedAt"`
// +listType=atomic
Collectables []InvestigationCollectable `json:"collectables"`
ViewMode InvestigationViewMode `json:"viewMode"`
}
// NewInvestigationSpec creates a new InvestigationSpec object.
func NewInvestigationSpec() *InvestigationSpec {
return &InvestigationSpec{
CreatedByProfile: *NewInvestigationPerson(),
Collectables: []InvestigationCollectable{},
ViewMode: *NewInvestigationViewMode(),
}
}
// +k8s:openapi-gen=true
type InvestigationV0alpha1TimeRangeRaw struct {
From string `json:"from"`
To string `json:"to"`
}
// NewInvestigationV0alpha1TimeRangeRaw creates a new InvestigationV0alpha1TimeRangeRaw object.
func NewInvestigationV0alpha1TimeRangeRaw() *InvestigationV0alpha1TimeRangeRaw {
return &InvestigationV0alpha1TimeRangeRaw{}
}
// +k8s:openapi-gen=true
type InvestigationViewModeMode string
const (
InvestigationViewModeModeCompact InvestigationViewModeMode = "compact"
InvestigationViewModeModeFull InvestigationViewModeMode = "full"
)

View File

@@ -1,80 +0,0 @@
package v0alpha1
import (
"context"
"github.com/grafana/grafana-app-sdk/resource"
)
type InvestigationIndexClient struct {
client *resource.TypedClient[*InvestigationIndex, *InvestigationIndexList]
}
func NewInvestigationIndexClient(client resource.Client) *InvestigationIndexClient {
return &InvestigationIndexClient{
client: resource.NewTypedClient[*InvestigationIndex, *InvestigationIndexList](client, InvestigationIndexKind()),
}
}
func NewInvestigationIndexClientFromGenerator(generator resource.ClientGenerator) (*InvestigationIndexClient, error) {
c, err := generator.ClientFor(InvestigationIndexKind())
if err != nil {
return nil, err
}
return NewInvestigationIndexClient(c), nil
}
func (c *InvestigationIndexClient) Get(ctx context.Context, identifier resource.Identifier) (*InvestigationIndex, error) {
return c.client.Get(ctx, identifier)
}
func (c *InvestigationIndexClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationIndexList, error) {
return c.client.List(ctx, namespace, opts)
}
func (c *InvestigationIndexClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*InvestigationIndexList, error) {
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
for resp.GetContinue() != "" {
page, err := c.client.List(ctx, namespace, resource.ListOptions{
Continue: resp.GetContinue(),
ResourceVersion: opts.ResourceVersion,
Limit: opts.Limit,
LabelFilters: opts.LabelFilters,
FieldSelectors: opts.FieldSelectors,
})
if err != nil {
return nil, err
}
resp.SetContinue(page.GetContinue())
resp.SetResourceVersion(page.GetResourceVersion())
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
}
return resp, nil
}
func (c *InvestigationIndexClient) Create(ctx context.Context, obj *InvestigationIndex, opts resource.CreateOptions) (*InvestigationIndex, error) {
// Make sure apiVersion and kind are set
obj.APIVersion = GroupVersion.Identifier()
obj.Kind = InvestigationIndexKind().Kind()
return c.client.Create(ctx, obj, opts)
}
func (c *InvestigationIndexClient) Update(ctx context.Context, obj *InvestigationIndex, opts resource.UpdateOptions) (*InvestigationIndex, error) {
return c.client.Update(ctx, obj, opts)
}
func (c *InvestigationIndexClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*InvestigationIndex, error) {
return c.client.Patch(ctx, identifier, req, opts)
}
func (c *InvestigationIndexClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
return c.client.Delete(ctx, identifier, opts)
}

View File

@@ -1,28 +0,0 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"encoding/json"
"io"
"github.com/grafana/grafana-app-sdk/resource"
)
// InvestigationIndexJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
type InvestigationIndexJSONCodec struct{}
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
func (*InvestigationIndexJSONCodec) Read(reader io.Reader, into resource.Object) error {
return json.NewDecoder(reader).Decode(into)
}
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
func (*InvestigationIndexJSONCodec) Write(writer io.Writer, from resource.Object) error {
return json.NewEncoder(writer).Encode(from)
}
// Interface compliance checks
var _ resource.Codec = &InvestigationIndexJSONCodec{}

View File

@@ -1,31 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
time "time"
)
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
type InvestigationIndexMetadata struct {
UpdateTimestamp time.Time `json:"updateTimestamp"`
CreatedBy string `json:"createdBy"`
Uid string `json:"uid"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
ResourceVersion string `json:"resourceVersion"`
Generation int64 `json:"generation"`
UpdatedBy string `json:"updatedBy"`
Labels map[string]string `json:"labels"`
}
// NewInvestigationIndexMetadata creates a new InvestigationIndexMetadata object.
func NewInvestigationIndexMetadata() *InvestigationIndexMetadata {
return &InvestigationIndexMetadata{
Finalizers: []string{},
Labels: map[string]string{},
}
}

View File

@@ -1,293 +0,0 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"fmt"
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"time"
)
// +k8s:openapi-gen=true
type InvestigationIndex struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
// Spec is the spec of the InvestigationIndex
Spec InvestigationIndexSpec `json:"spec" yaml:"spec"`
}
func (o *InvestigationIndex) GetSpec() any {
return o.Spec
}
func (o *InvestigationIndex) SetSpec(spec any) error {
cast, ok := spec.(InvestigationIndexSpec)
if !ok {
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
}
o.Spec = cast
return nil
}
func (o *InvestigationIndex) GetSubresources() map[string]any {
return map[string]any{}
}
func (o *InvestigationIndex) GetSubresource(name string) (any, bool) {
switch name {
default:
return nil, false
}
}
func (o *InvestigationIndex) SetSubresource(name string, value any) error {
switch name {
default:
return fmt.Errorf("subresource '%s' does not exist", name)
}
}
func (o *InvestigationIndex) GetStaticMetadata() resource.StaticMetadata {
gvk := o.GroupVersionKind()
return resource.StaticMetadata{
Name: o.ObjectMeta.Name,
Namespace: o.ObjectMeta.Namespace,
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
func (o *InvestigationIndex) SetStaticMetadata(metadata resource.StaticMetadata) {
o.Name = metadata.Name
o.Namespace = metadata.Namespace
o.SetGroupVersionKind(schema.GroupVersionKind{
Group: metadata.Group,
Version: metadata.Version,
Kind: metadata.Kind,
})
}
func (o *InvestigationIndex) GetCommonMetadata() resource.CommonMetadata {
dt := o.DeletionTimestamp
var deletionTimestamp *time.Time
if dt != nil {
deletionTimestamp = &dt.Time
}
// Legacy ExtraFields support
extraFields := make(map[string]any)
if o.Annotations != nil {
extraFields["annotations"] = o.Annotations
}
if o.ManagedFields != nil {
extraFields["managedFields"] = o.ManagedFields
}
if o.OwnerReferences != nil {
extraFields["ownerReferences"] = o.OwnerReferences
}
return resource.CommonMetadata{
UID: string(o.UID),
ResourceVersion: o.ResourceVersion,
Generation: o.Generation,
Labels: o.Labels,
CreationTimestamp: o.CreationTimestamp.Time,
DeletionTimestamp: deletionTimestamp,
Finalizers: o.Finalizers,
UpdateTimestamp: o.GetUpdateTimestamp(),
CreatedBy: o.GetCreatedBy(),
UpdatedBy: o.GetUpdatedBy(),
ExtraFields: extraFields,
}
}
func (o *InvestigationIndex) SetCommonMetadata(metadata resource.CommonMetadata) {
o.UID = types.UID(metadata.UID)
o.ResourceVersion = metadata.ResourceVersion
o.Generation = metadata.Generation
o.Labels = metadata.Labels
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
if metadata.DeletionTimestamp != nil {
dt := metav1.NewTime(*metadata.DeletionTimestamp)
o.DeletionTimestamp = &dt
} else {
o.DeletionTimestamp = nil
}
o.Finalizers = metadata.Finalizers
if o.Annotations == nil {
o.Annotations = make(map[string]string)
}
if !metadata.UpdateTimestamp.IsZero() {
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
}
if metadata.CreatedBy != "" {
o.SetCreatedBy(metadata.CreatedBy)
}
if metadata.UpdatedBy != "" {
o.SetUpdatedBy(metadata.UpdatedBy)
}
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
if metadata.ExtraFields != nil {
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
if cast, ok := annotations.(map[string]string); ok {
o.Annotations = cast
}
}
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
o.ManagedFields = cast
}
}
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
o.OwnerReferences = cast
}
}
}
}
func (o *InvestigationIndex) GetCreatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
}
func (o *InvestigationIndex) SetCreatedBy(createdBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
}
func (o *InvestigationIndex) GetUpdateTimestamp() time.Time {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
return parsed
}
func (o *InvestigationIndex) SetUpdateTimestamp(updateTimestamp time.Time) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
}
func (o *InvestigationIndex) GetUpdatedBy() string {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
}
func (o *InvestigationIndex) SetUpdatedBy(updatedBy string) {
if o.ObjectMeta.Annotations == nil {
o.ObjectMeta.Annotations = make(map[string]string)
}
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
}
func (o *InvestigationIndex) Copy() resource.Object {
return resource.CopyObject(o)
}
func (o *InvestigationIndex) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *InvestigationIndex) DeepCopy() *InvestigationIndex {
cpy := &InvestigationIndex{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *InvestigationIndex) DeepCopyInto(dst *InvestigationIndex) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
o.Spec.DeepCopyInto(&dst.Spec)
}
// Interface compliance compile-time check
var _ resource.Object = &InvestigationIndex{}
// +k8s:openapi-gen=true
type InvestigationIndexList struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ListMeta `json:"metadata" yaml:"metadata"`
Items []InvestigationIndex `json:"items" yaml:"items"`
}
func (o *InvestigationIndexList) DeepCopyObject() runtime.Object {
return o.Copy()
}
func (o *InvestigationIndexList) Copy() resource.ListObject {
cpy := &InvestigationIndexList{
TypeMeta: o.TypeMeta,
Items: make([]InvestigationIndex, len(o.Items)),
}
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
for i := 0; i < len(o.Items); i++ {
if item, ok := o.Items[i].Copy().(*InvestigationIndex); ok {
cpy.Items[i] = *item
}
}
return cpy
}
func (o *InvestigationIndexList) GetItems() []resource.Object {
items := make([]resource.Object, len(o.Items))
for i := 0; i < len(o.Items); i++ {
items[i] = &o.Items[i]
}
return items
}
func (o *InvestigationIndexList) SetItems(items []resource.Object) {
o.Items = make([]InvestigationIndex, len(items))
for i := 0; i < len(items); i++ {
o.Items[i] = *items[i].(*InvestigationIndex)
}
}
func (o *InvestigationIndexList) DeepCopy() *InvestigationIndexList {
cpy := &InvestigationIndexList{}
o.DeepCopyInto(cpy)
return cpy
}
func (o *InvestigationIndexList) DeepCopyInto(dst *InvestigationIndexList) {
resource.CopyObjectInto(dst, o)
}
// Interface compliance compile-time check
var _ resource.ListObject = &InvestigationIndexList{}
// Copy methods for all subresource types
// DeepCopy creates a full deep copy of Spec
func (s *InvestigationIndexSpec) DeepCopy() *InvestigationIndexSpec {
cpy := &InvestigationIndexSpec{}
s.DeepCopyInto(cpy)
return cpy
}
// DeepCopyInto deep copies Spec into another Spec object
func (s *InvestigationIndexSpec) DeepCopyInto(dst *InvestigationIndexSpec) {
resource.CopyObjectInto(dst, s)
}

View File

@@ -1,34 +0,0 @@
//
// Code generated by grafana-app-sdk. DO NOT EDIT.
//
package v0alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
)
// schema is unexported to prevent accidental overwrites
var (
schemaInvestigationIndex = resource.NewSimpleSchema("investigations.grafana.app", "v0alpha1", &InvestigationIndex{}, &InvestigationIndexList{}, resource.WithKind("InvestigationIndex"),
resource.WithPlural("investigationindexes"), resource.WithScope(resource.NamespacedScope))
kindInvestigationIndex = resource.Kind{
Schema: schemaInvestigationIndex,
Codecs: map[resource.KindEncoding]resource.Codec{
resource.KindEncodingJSON: &InvestigationIndexJSONCodec{},
},
}
)
// Kind returns a resource.Kind for this Schema with a JSON codec
func InvestigationIndexKind() resource.Kind {
return kindInvestigationIndex
}
// Schema returns a resource.SimpleSchema representation of InvestigationIndex
func InvestigationIndexSchema() *resource.SimpleSchema {
return schemaInvestigationIndex
}
// Interface compliance checks
var _ resource.Schema = kindInvestigationIndex

View File

@@ -1,94 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// Person represents a user profile with basic information
// +k8s:openapi-gen=true
type InvestigationIndexPerson struct {
// Unique identifier for the user
Uid string `json:"uid"`
// Display name of the user
Name string `json:"name"`
// URL to user's Gravatar image
GravatarUrl string `json:"gravatarUrl"`
}
// NewInvestigationIndexPerson creates a new InvestigationIndexPerson object.
func NewInvestigationIndexPerson() *InvestigationIndexPerson {
return &InvestigationIndexPerson{}
}
// Type definition for investigation summaries
// +k8s:openapi-gen=true
type InvestigationIndexInvestigationSummary struct {
Title string `json:"title"`
CreatedByProfile InvestigationIndexPerson `json:"createdByProfile"`
HasCustomName bool `json:"hasCustomName"`
IsFavorite bool `json:"isFavorite"`
OverviewNote string `json:"overviewNote"`
OverviewNoteUpdatedAt string `json:"overviewNoteUpdatedAt"`
ViewMode InvestigationIndexViewMode `json:"viewMode"`
// +listType=atomic
CollectableSummaries []InvestigationIndexCollectableSummary `json:"collectableSummaries"`
}
// NewInvestigationIndexInvestigationSummary creates a new InvestigationIndexInvestigationSummary object.
func NewInvestigationIndexInvestigationSummary() *InvestigationIndexInvestigationSummary {
return &InvestigationIndexInvestigationSummary{
CreatedByProfile: *NewInvestigationIndexPerson(),
ViewMode: *NewInvestigationIndexViewMode(),
CollectableSummaries: []InvestigationIndexCollectableSummary{},
}
}
// +k8s:openapi-gen=true
type InvestigationIndexViewMode struct {
Mode InvestigationIndexViewModeMode `json:"mode"`
ShowComments bool `json:"showComments"`
ShowTooltips bool `json:"showTooltips"`
}
// NewInvestigationIndexViewMode creates a new InvestigationIndexViewMode object.
func NewInvestigationIndexViewMode() *InvestigationIndexViewMode {
return &InvestigationIndexViewMode{}
}
// +k8s:openapi-gen=true
type InvestigationIndexCollectableSummary struct {
Id string `json:"id"`
Title string `json:"title"`
LogoPath string `json:"logoPath"`
Origin string `json:"origin"`
}
// NewInvestigationIndexCollectableSummary creates a new InvestigationIndexCollectableSummary object.
func NewInvestigationIndexCollectableSummary() *InvestigationIndexCollectableSummary {
return &InvestigationIndexCollectableSummary{}
}
// +k8s:openapi-gen=true
type InvestigationIndexSpec struct {
// Title of the index, e.g. 'Favorites' or 'My Investigations'
Title string `json:"title"`
// The Person who owns this investigation index
Owner InvestigationIndexPerson `json:"owner"`
// Array of investigation summaries
// +listType=atomic
InvestigationSummaries []InvestigationIndexInvestigationSummary `json:"investigationSummaries"`
}
// NewInvestigationIndexSpec creates a new InvestigationIndexSpec object.
func NewInvestigationIndexSpec() *InvestigationIndexSpec {
return &InvestigationIndexSpec{
Owner: *NewInvestigationIndexPerson(),
InvestigationSummaries: []InvestigationIndexInvestigationSummary{},
}
}
// +k8s:openapi-gen=true
type InvestigationIndexViewModeMode string
const (
InvestigationIndexViewModeModeCompact InvestigationIndexViewModeMode = "compact"
InvestigationIndexViewModeModeFull InvestigationIndexViewModeMode = "full"
)

View File

@@ -1,44 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// +k8s:openapi-gen=true
type InvestigationIndexstatusOperatorState struct {
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State InvestigationIndexStatusOperatorStateState `json:"state"`
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]interface{} `json:"details,omitempty"`
}
// NewInvestigationIndexstatusOperatorState creates a new InvestigationIndexstatusOperatorState object.
func NewInvestigationIndexstatusOperatorState() *InvestigationIndexstatusOperatorState {
return &InvestigationIndexstatusOperatorState{}
}
// +k8s:openapi-gen=true
type InvestigationIndexStatus struct {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]InvestigationIndexstatusOperatorState `json:"operatorStates,omitempty"`
// additionalFields is reserved for future use
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
}
// NewInvestigationIndexStatus creates a new InvestigationIndexStatus object.
func NewInvestigationIndexStatus() *InvestigationIndexStatus {
return &InvestigationIndexStatus{}
}
// +k8s:openapi-gen=true
type InvestigationIndexStatusOperatorStateState string
const (
InvestigationIndexStatusOperatorStateStateSuccess InvestigationIndexStatusOperatorStateState = "success"
InvestigationIndexStatusOperatorStateStateInProgress InvestigationIndexStatusOperatorStateState = "in_progress"
InvestigationIndexStatusOperatorStateStateFailed InvestigationIndexStatusOperatorStateState = "failed"
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,136 +0,0 @@
//
// This file is generated by grafana-app-sdk
// DO NOT EDIT
//
package apis
import (
"encoding/json"
"fmt"
"strings"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
v0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1"
)
var (
rawSchemaInvestigationv0alpha1 = []byte(`{"Collectable":{"additionalProperties":false,"description":"Collectable represents an item collected during investigation","properties":{"createdAt":{"type":"string"},"datasource":{"$ref":"#/components/schemas/DatasourceRef"},"fieldConfig":{"type":"string"},"id":{"type":"string"},"logoPath":{"type":"string"},"note":{"type":"string"},"noteUpdatedAt":{"type":"string"},"origin":{"type":"string"},"queries":{"description":"+listType=atomic","items":{"type":"string"},"type":"array"},"timeRange":{"$ref":"#/components/schemas/TimeRange"},"title":{"type":"string"},"type":{"type":"string"},"url":{"type":"string"}},"required":["id","createdAt","title","origin","type","queries","timeRange","datasource","url","note","noteUpdatedAt","fieldConfig"],"type":"object"},"DatasourceRef":{"additionalProperties":false,"description":"DatasourceRef is a reference to a datasource","properties":{"uid":{"type":"string"}},"required":["uid"],"type":"object"},"Investigation":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"TimeRange":{"additionalProperties":false,"description":"TimeRange represents a time range with both absolute and relative values","properties":{"from":{"type":"string"},"raw":{"additionalProperties":false,"properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"type":"object"},"to":{"type":"string"}},"required":["from","to","raw"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"collectables":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/Collectable"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","collectables","viewMode"],"type":"object"}}`)
versionSchemaInvestigationv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaInvestigationv0alpha1, &versionSchemaInvestigationv0alpha1)
rawSchemaInvestigationIndexv0alpha1 = []byte(`{"CollectableSummary":{"additionalProperties":false,"properties":{"id":{"type":"string"},"logoPath":{"type":"string"},"origin":{"type":"string"},"title":{"type":"string"}},"required":["id","title","logoPath","origin"],"type":"object"},"InvestigationIndex":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"InvestigationSummary":{"additionalProperties":false,"description":"Type definition for investigation summaries","properties":{"collectableSummaries":{"description":"+listType=atomic","items":{"$ref":"#/components/schemas/CollectableSummary"},"type":"array"},"createdByProfile":{"$ref":"#/components/schemas/Person"},"hasCustomName":{"type":"boolean"},"isFavorite":{"type":"boolean"},"overviewNote":{"type":"string"},"overviewNoteUpdatedAt":{"type":"string"},"title":{"type":"string"},"viewMode":{"$ref":"#/components/schemas/ViewMode"}},"required":["title","createdByProfile","hasCustomName","isFavorite","overviewNote","overviewNoteUpdatedAt","viewMode","collectableSummaries"],"type":"object"},"Person":{"additionalProperties":false,"description":"Person represents a user profile with basic information","properties":{"gravatarUrl":{"description":"URL to user's Gravatar image","type":"string"},"name":{"description":"Display name of the user","type":"string"},"uid":{"description":"Unique identifier for the user","type":"string"}},"required":["uid","name","gravatarUrl"],"type":"object"},"ViewMode":{"additionalProperties":false,"properties":{"mode":{"enum":["compact","full"],"type":"string"},"showComments":{"type":"boolean"},"showTooltips":{"type":"boolean"}},"required":["mode","showComments","showTooltips"],"type":"object"},"spec":{"additionalProperties":false,"properties":{"investigationSummaries":{"description":"Array of investigation summaries\n+listType=atomic","items":{"$ref":"#/components/schemas/InvestigationSummary"},"type":"array"},"owner":{"$ref":"#/components/schemas/Person","description":"The Person who owns this investigation index"},"title":{"description":"Title of the index, e.g. 'Favorites' or 'My Investigations'","type":"string"}},"required":["title","owner","investigationSummaries"],"type":"object"}}`)
versionSchemaInvestigationIndexv0alpha1 app.VersionSchema
_ = json.Unmarshal(rawSchemaInvestigationIndexv0alpha1, &versionSchemaInvestigationIndexv0alpha1)
)
var appManifestData = app.ManifestData{
AppName: "investigations",
Group: "investigations.grafana.app",
PreferredVersion: "v0alpha1",
Versions: []app.ManifestVersion{
{
Name: "v0alpha1",
Served: true,
Kinds: []app.ManifestVersionKind{
{
Kind: "Investigation",
Plural: "Investigations",
Scope: "Namespaced",
Conversion: false,
Schema: &versionSchemaInvestigationv0alpha1,
},
{
Kind: "InvestigationIndex",
Plural: "InvestigationIndexes",
Scope: "Namespaced",
Conversion: false,
Schema: &versionSchemaInvestigationIndexv0alpha1,
},
},
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("investigations")
}
var kindVersionToGoType = map[string]resource.Kind{
"Investigation/v0alpha1": v0alpha1.InvestigationKind(),
"InvestigationIndex/v0alpha1": v0alpha1.InvestigationIndexKind(),
}
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
// If there is no association for the provided Kind and Version, exists will return false.
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
return goType, exists
}
var customRouteToGoResponseType = map[string]any{}
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
// If there is no association for the provided kind, version, custom route path, and method, exists will return false.
// Resource routes (those without a kind) should prefix their route with "<namespace>/" if the route is namespaced (otherwise the route is assumed to be cluster-scope)
func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoParamsType = map[string]runtime.Object{}
func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoRequestBodyType = map[string]any{}
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
type GoTypeAssociator struct{}
func NewGoTypeAssociator() *GoTypeAssociator {
return &GoTypeAssociator{}
}
func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) {
return ManifestGoTypeAssociator(kind, version)
}
func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteResponsesAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) {
return ManifestCustomRouteQueryAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb)
}

View File

@@ -1,62 +0,0 @@
package app
import (
"context"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/operator"
"github.com/grafana/grafana-app-sdk/resource"
"github.com/grafana/grafana-app-sdk/simple"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
investigationsv0alpha1 "github.com/grafana/grafana/apps/investigations/pkg/apis/investigations/v0alpha1"
)
func New(cfg app.Config) (app.App, error) {
var err error
simpleConfig := simple.AppConfig{
Name: "investigation",
KubeConfig: cfg.KubeConfig,
InformerConfig: simple.AppInformerConfig{
InformerOptions: operator.InformerOptions{
ErrorHandler: func(_ context.Context, err error) {
klog.ErrorS(err, "Informer processing error")
},
},
},
ManagedKinds: []simple.AppManagedKind{
{
Kind: investigationsv0alpha1.InvestigationKind(),
},
{
Kind: investigationsv0alpha1.InvestigationIndexKind(),
},
},
}
a, err := simple.NewApp(simpleConfig)
if err != nil {
return nil, err
}
err = a.ValidateManifest(cfg.ManifestData)
if err != nil {
return nil, err
}
return a, nil
}
func GetKinds() map[schema.GroupVersion][]resource.Kind {
gv := schema.GroupVersion{
Group: investigationsv0alpha1.InvestigationKind().Group(),
Version: investigationsv0alpha1.InvestigationKind().Version(),
}
return map[schema.GroupVersion][]resource.Kind{
gv: {
investigationsv0alpha1.InvestigationKind(),
investigationsv0alpha1.InvestigationIndexKind(),
},
}
}

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