From 8e9675ce1cfecaba5d34f2a09fd1021f5c29af68 Mon Sep 17 00:00:00 2001 From: alexandra vargas Date: Tue, 30 Dec 2025 15:37:34 +0100 Subject: [PATCH] Create dashboard validator app - Generate scaffolding - Create DashboardCompatibilityScore Kind in CUE --- apps/dashvalidator/Makefile | 102 ++++++ ...tyscore.dashvalidator.ext.grafana.com.json | 228 ++++++++++++ .../definitions/dashvalidator-manifest.json | 223 ++++++++++++ apps/dashvalidator/go.mod | 3 + apps/dashvalidator/go.sum | 1 + apps/dashvalidator/kinds/cue.mod/module.cue | 2 + .../kinds/dashboardcompatibilityscore.cue | 157 +++++++++ apps/dashvalidator/kinds/manifest.cue | 67 ++++ apps/dashvalidator/local/Tiltfile | 77 +++++ apps/dashvalidator/local/config.yaml | 47 +++ apps/dashvalidator/local/scripts/cluster.sh | 43 +++ .../dashvalidator/local/scripts/push_image.sh | 12 + .../v1alpha1/constants.go | 18 + .../dashboardcompatibilityscore_client_gen.go | 99 ++++++ .../dashboardcompatibilityscore_codec_gen.go | 28 ++ ...ashboardcompatibilityscore_metadata_gen.go | 31 ++ .../dashboardcompatibilityscore_object_gen.go | 326 ++++++++++++++++++ .../dashboardcompatibilityscore_schema_gen.go | 34 ++ .../dashboardcompatibilityscore_spec_gen.go | 48 +++ .../dashboardcompatibilityscore_status_gen.go | 151 ++++++++ .../manifestdata/dashvalidator_manifest.go | 124 +++++++ .../dashboardcompatibilityscore_object_gen.ts | 49 +++ .../v1alpha1/types.metadata.gen.ts | 30 ++ .../v1alpha1/types.spec.gen.ts | 42 +++ .../v1alpha1/types.status.gen.ts | 142 ++++++++ 25 files changed, 2084 insertions(+) create mode 100644 apps/dashvalidator/Makefile create mode 100644 apps/dashvalidator/definitions/dashboardcompatibilityscore.dashvalidator.ext.grafana.com.json create mode 100644 apps/dashvalidator/definitions/dashvalidator-manifest.json create mode 100644 apps/dashvalidator/go.mod create mode 100644 apps/dashvalidator/go.sum create mode 100644 apps/dashvalidator/kinds/cue.mod/module.cue create mode 100644 apps/dashvalidator/kinds/dashboardcompatibilityscore.cue create mode 100644 apps/dashvalidator/kinds/manifest.cue create mode 100644 apps/dashvalidator/local/Tiltfile create mode 100644 apps/dashvalidator/local/config.yaml create mode 100755 apps/dashvalidator/local/scripts/cluster.sh create mode 100755 apps/dashvalidator/local/scripts/push_image.sh create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/constants.go create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_client_gen.go create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_codec_gen.go create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_metadata_gen.go create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.go create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_schema_gen.go create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_spec_gen.go create mode 100644 apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_status_gen.go create mode 100644 apps/dashvalidator/pkg/generated/manifestdata/dashvalidator_manifest.go create mode 100644 apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.ts create mode 100644 apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.metadata.gen.ts create mode 100644 apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.spec.gen.ts create mode 100644 apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.status.gen.ts diff --git a/apps/dashvalidator/Makefile b/apps/dashvalidator/Makefile new file mode 100644 index 00000000000..d064a3861c4 --- /dev/null +++ b/apps/dashvalidator/Makefile @@ -0,0 +1,102 @@ +# FIXME: This Makefile was generated by `grafana-app-sdk project init`, +# With all possible pre-made make targets, and should be customized to your project based on your needs. + +SOURCES := $(shell find . -type f -name "*.go") +MOD_FILES := go.mod go.sum +VENDOR := vendor +COVOUT := coverage.out + +OPERATOR_DOCKERIMAGE := "dashvalidator" + +.PHONY: all +all: deps lint test build + +.PHONY: deps +deps: $(VENDOR) + +.PHONY: lint +lint: + golangci-lint run --max-same-issues=0 --max-issues-per-linter=0 + +.PHONY: test +test: + go test -count=1 -cover -covermode=atomic -coverprofile=$(COVOUT) ./... + +.PHONY: coverage +coverage: test + go tool cover -html=$(COVOUT) + +.PHONY: build +build: build/plugin build/operator + +.PHONY: build/plugin +build/plugin: build/plugin-backend build/plugin-frontend + +.PHONY: build/plugin-frontend +build/plugin-frontend: +ifeq ("$(wildcard plugin/src/plugin.json)","plugin/src/plugin.json") + @cd plugin && yarn install && yarn build +else + @echo "No plugin.json found, skipping frontend build" +endif + +.PHONY: build/plugin-backend +build/plugin-backend: +ifeq ("$(wildcard plugin/Magefile.go)","plugin/Magefile.go") + @cd plugin && mage -v +else + @echo "No Magefile.go found, skipping backend build" +endif + +.PHONY: build/operator +build/operator: + docker build -t $(OPERATOR_DOCKERIMAGE) -f cmd/operator/Dockerfile . + +.PHONY: compile/operator +compile/operator: + @go build -o "target/operator" cmd/operator/*.go + +.PHONY: generate +generate: + @grafana-app-sdk generate --source kinds --format cue --crdmanifest + +.PHONY: local/up +local/up: local/generate + @local/scripts/cluster.sh create "local/generated/k3d-config.json" + @cd local && tilt up + +.PHONY: local/generate +local/generate: + @grafana-app-sdk project local generate + +.PHONY: local/down +local/down: + @cd local && tilt down + +.PHONY: local/deploy_plugin +local/deploy_plugin: + -tilt disable grafana + @mkdir -p local/mounted-files/plugin/dist + cp -R plugin/dist/* local/mounted-files/plugin/dist/ + -tilt enable grafana + +.PHONY: local/push_operator +local/push_operator: + # Tag the docker image as part of localhost, which is what the generated k8s uses to avoid confusion with the real operator image + @docker tag "$(OPERATOR_DOCKERIMAGE):latest" "localhost/$(OPERATOR_DOCKERIMAGE):latest" + @local/scripts/push_image.sh "localhost/$(OPERATOR_DOCKERIMAGE):latest" + +.PHONY: local/clean +local/clean: local/down + @local/scripts/cluster.sh delete + +.PHONY: clean +clean: + @rm -f $(COVOUT) + @rm -rf $(VENDOR) + +.PHONY: $(VENDOR) +$(VENDOR): $(SOURCES) $(MOD_FILES) + @go mod tidy + @go mod vendor + @touch $@ diff --git a/apps/dashvalidator/definitions/dashboardcompatibilityscore.dashvalidator.ext.grafana.com.json b/apps/dashvalidator/definitions/dashboardcompatibilityscore.dashvalidator.ext.grafana.com.json new file mode 100644 index 00000000000..ea5c492a32d --- /dev/null +++ b/apps/dashvalidator/definitions/dashboardcompatibilityscore.dashvalidator.ext.grafana.com.json @@ -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" + } +} diff --git a/apps/dashvalidator/definitions/dashvalidator-manifest.json b/apps/dashvalidator/definitions/dashvalidator-manifest.json new file mode 100644 index 00000000000..7ccdf5cb3b3 --- /dev/null +++ b/apps/dashvalidator/definitions/dashvalidator-manifest.json @@ -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" + } +} diff --git a/apps/dashvalidator/go.mod b/apps/dashvalidator/go.mod new file mode 100644 index 00000000000..812c0fad781 --- /dev/null +++ b/apps/dashvalidator/go.mod @@ -0,0 +1,3 @@ +module github.com/grafana/grafana/apps/dashvalidator + +go 1.22 diff --git a/apps/dashvalidator/go.sum b/apps/dashvalidator/go.sum new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/apps/dashvalidator/go.sum @@ -0,0 +1 @@ + diff --git a/apps/dashvalidator/kinds/cue.mod/module.cue b/apps/dashvalidator/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..2d2e147c870 --- /dev/null +++ b/apps/dashvalidator/kinds/cue.mod/module.cue @@ -0,0 +1,2 @@ +module: "github.com/grafana/grafana/apps/dashvalidator/kinds" +language: version: "v0.8.2" diff --git a/apps/dashvalidator/kinds/dashboardcompatibilityscore.cue b/apps/dashvalidator/kinds/dashboardcompatibilityscore.cue new file mode 100644 index 00000000000..e3dde3d1f77 --- /dev/null +++ b/apps/dashvalidator/kinds/dashboardcompatibilityscore.cue @@ -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 +} diff --git a/apps/dashvalidator/kinds/manifest.cue b/apps/dashvalidator/kinds/manifest.cue new file mode 100644 index 00000000000..728b95b89b7 --- /dev/null +++ b/apps/dashvalidator/kinds/manifest.cue @@ -0,0 +1,67 @@ +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: foo.ext.grafana.app + + // versions is a map of versions supported by your app. Version names should follow the format "v" or + // "v(alpha|beta)". 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, v1alpha1) instead. + 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 + } + } +} diff --git a/apps/dashvalidator/local/Tiltfile b/apps/dashvalidator/local/Tiltfile new file mode 100644 index 00000000000..51b74ee5ff3 --- /dev/null +++ b/apps/dashvalidator/local/Tiltfile @@ -0,0 +1,77 @@ + +# version_settings() enforces a minimum Tilt version +# https://docs.tilt.dev/api.html#api.version_settings +version_settings(constraint='>=0.22.2') + +def name(c): + return c['metadata']['name'] + +def namespace(c): + if 'namespace' in c['metadata']: + return c['metadata']['namespace'] + return '' + +def decode(yaml): + resources = decode_yaml_stream(yaml) + + # workaround a bug in decode_yaml_stream where it returns duplicates + # This bug has been fixed in Tilt v0.17.3+ + filtered = [] + names = {} + for r in resources: + if r == None: + continue + + n = '%s:%s:%s' % (name(r), r['kind'], namespace(r)) + if n in names: + continue + + names[n] = True + filtered.append(r) + + return filtered + +def get_label(o, lbl): + if 'labels' in o['metadata'] and lbl in o['metadata']['labels']: + return o['metadata']['labels'][lbl] + return '' + +def find_overlapping(o, yamls): + for elem in yamls: + if name(o) == name(elem) and o['kind'] == elem['kind'] and namespace(o) == namespace(elem): + return elem + return None + +# Parse all YAML files in our "generated" directory +yaml_objects = [] +for filename in listdir('generated'): + if filename.lower().endswith(('.yaml', '.yml')): + decoded = decode(read_file(filename)) + yaml_objects += decoded +# Next, allow for additional yamls which can optionally override the generated yamls +for filename in listdir('additional'): + if filename.lower().endswith(('.yaml', '.yml')): + decoded = decode(read_file(filename)) + for o in decoded: + present = find_overlapping(o, yaml_objects) + if present != None: + yaml_objects.remove(present) + yaml_objects += decoded + +bundle = encode_yaml_stream(yaml_objects) + +# k8s_yaml automatically creates resources in Tilt for the entities +# and will inject any images referenced in the Tiltfile when deploying +# https://docs.tilt.dev/api.html#api.k8s_yaml +k8s_yaml(bundle) + +# Group CRD's together +crds = [r for r in yaml_objects if (r['kind'] == 'CustomResourceDefinition')] +if len(crds) > 0: + k8s_resource(new_name='CustomResourceDefinitions', objects=[('%s' % name(r)) for r in yaml_objects if (r['kind'] == 'CustomResourceDefinition')], resource_deps=['uncategorized']) + +# Make webhooks dependent on all services starting +services = [('%s' % name(r)) for r in yaml_objects if r['kind'] == 'Service'] +webhooks = [r for r in yaml_objects if (r['kind'] == 'ValidatingWebhookConfiguration' or r['kind'] == 'MutatingWebhookConfiguration')] +if len(webhooks) > 0: + k8s_resource(new_name='Webhooks', objects=[('%s' % name(r)) for r in webhooks], resource_deps=services) diff --git a/apps/dashvalidator/local/config.yaml b/apps/dashvalidator/local/config.yaml new file mode 100644 index 00000000000..eee2686f4fe --- /dev/null +++ b/apps/dashvalidator/local/config.yaml @@ -0,0 +1,47 @@ +# Port used to bind services to localhost, for example, grafana will be available at http://grafana.k3d.localhost:9999 +port: 9999 +# Port used for the kubernetes APIServer on localhost +kubePort: 8556 +# Pre-configured datasources to install on grafana. For custom-configuration datasources, use `datasourceConfigs` +datasources: + - cortex + - tempo + - loki +# Plugin JSON data, as key/value pairs +pluginJson: + foo: bar +# Plugin Secure JSON data. By default, the `kubeconfig` and `kubenamespace` values will be added in the generated YAML. +# You can overwrite those values by specifying them here instead. +pluginSecureJson: + baz: foo +# Standalone operator docker image. Leave this empty to not deploy an operator +operatorImage: 'dashvalidator:latest' +# Governs whether the local setup generates kubernetes manifests for varying kinds of webhooks attached to your operator and each CRD +webhooks: + # The port the operator exposes an HTTPS server with the webhook endpoint(s) on + port: 8443 +# Non-standard or additional datasources you want to automatically include in grafana's provisioned list +# The actual datasources need to be set up manually (arbitrary kubernetes yamls can be added to the local setup via the 'additional' folder), +# but you can predefine the connection details so they'll be added to the local grafana. +datasourceConfigs: +# Here is an example cortex config +# - access: proxy +# editable: false +# name: "my-cortex-datasource" +# type: prometheus +# uid: "my-cortex-datasource" +# url: "http://cortex.default.svc.cluster.local:9009/api/prom" +# +# Toggle the generating of the grafana deployments, if you want to control these elsewhere +generateGrafanaDeployment: true + +# which grafana image to use +grafanaImage: grafana/grafana-enterprise:main + +# Install plugins from other sources (URLS). See https://grafana.com/docs/grafana/latest/setup-grafana/configure-docker/#install-plugins-from-other-sources +grafanaInstallPlugins: '' + +# You can mount additional volumes from the local disk (aside from the already-mounted ./local/mounted-files) by specifying them here +additionalVolumeMounts: +# - sourcePath: "./local/other" # Paths starting with a ./ or no leading slash are relative to the project root. Start with / for an absolute path +# mountPath: "/tmp/k3d/other" # The destination volume path. Best practice is to use /tmp/k3d as the starting point. The default volume mount for ./local/mounted-files is /tmp/k3d/mounted-files diff --git a/apps/dashvalidator/local/scripts/cluster.sh b/apps/dashvalidator/local/scripts/cluster.sh new file mode 100755 index 00000000000..34652f160a8 --- /dev/null +++ b/apps/dashvalidator/local/scripts/cluster.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -eufo pipefail + +CLUSTER_NAME="dashvalidator" + +create_cluster() { + K3D_CONFIG="${1:-generated/k3d-config.json}" + + if ! k3d cluster list "${CLUSTER_NAME}" >/dev/null 2>&1; then + # Array of extra options to add to the k3d cluster create command + EXTRA_K3D_OPTS=() + + # Bug in k3d for btrfs filesystems workaround, see https://k3d.io/v5.2.2/faq/faq/#issues-with-btrfs + # Apple is APFS/HFS and stat has a different API, so we might as well skip that + if [[ "${OSTYPE}" != "darwin*" ]]; then + ROOTFS="$(stat -f --format="%T" "/")" + if [[ "${ROOTFS}" == "btrfs" ]]; then + EXTRA_K3D_OPTS+=("-v" "/dev/mapper:/dev/mapper") + fi + fi + + k3d cluster create "${CLUSTER_NAME}" --config "${K3D_CONFIG}" ${EXTRA_K3D_OPTS[@]+"${EXTRA_K3D_OPTS[@]}"} + else + echo "Cluster already exists" + fi +} + +delete_cluster() { + k3d cluster delete "${CLUSTER_NAME}" +} + +if [ $# -lt 1 ]; then + echo "Usage: ./cluster.sh [create|delete]" + exit 1 +fi + +if [ $1 == "create" ]; then + create_cluster $2 +elif [ $1 == "delete" ]; then + delete_cluster +else + echo "Unknown argument ${1}" +fi diff --git a/apps/dashvalidator/local/scripts/push_image.sh b/apps/dashvalidator/local/scripts/push_image.sh new file mode 100755 index 00000000000..97abb7105de --- /dev/null +++ b/apps/dashvalidator/local/scripts/push_image.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -eufo pipefail + +CLUSTER_NAME="dashvalidator" +IMAGE="$1" + +if [[ "$IMAGE" == "" ]]; then + echo "usage: push_image.sh " + exit 1 +fi + +k3d image import "${IMAGE}" -c "${CLUSTER_NAME}" diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/constants.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/constants.go new file mode 100644 index 00000000000..f70011f20d0 --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/constants.go @@ -0,0 +1,18 @@ +package v1alpha1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "dashvalidator.ext.grafana.com" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1alpha1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_client_gen.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_client_gen.go new file mode 100644 index 00000000000..c5ee74324ef --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_client_gen.go @@ -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, Kind()), + } +} + +func NewDashboardCompatibilityScoreClientFromGenerator(generator resource.ClientGenerator) (*DashboardCompatibilityScoreClient, error) { + c, err := generator.ClientFor(Kind()) + 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 = Kind().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 Status, opts resource.UpdateOptions) (*DashboardCompatibilityScore, error) { + return c.client.Update(ctx, &DashboardCompatibilityScore{ + TypeMeta: metav1.TypeMeta{ + Kind: Kind().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) +} diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_codec_gen.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_codec_gen.go new file mode 100644 index 00000000000..bb458caeb88 --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// JSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type JSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*JSONCodec) 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 { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &JSONCodec{} diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_metadata_gen.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_metadata_gen.go new file mode 100644 index 00000000000..cb7233b22ab --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1alpha1 + +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 Metadata 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"` +} + +// NewMetadata creates a new Metadata object. +func NewMetadata() *Metadata { + return &Metadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.go new file mode 100644 index 00000000000..f982743fb98 --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.go @@ -0,0 +1,326 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1alpha1 + +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 DashboardCompatibilityScore struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the DashboardCompatibilityScore + Spec Spec `json:"spec" yaml:"spec"` + + Status Status `json:"status" yaml:"status"` +} + +func NewDashboardCompatibilityScore() *DashboardCompatibilityScore { + return &DashboardCompatibilityScore{ + Spec: *NewSpec(), + Status: *NewStatus(), + } +} + +func (o *DashboardCompatibilityScore) GetSpec() any { + return o.Spec +} + +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) + } + o.Spec = cast + return nil +} + +func (o *DashboardCompatibilityScore) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *DashboardCompatibilityScore) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *DashboardCompatibilityScore) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(Status) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type Status", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *DashboardCompatibilityScore) 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 *DashboardCompatibilityScore) 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 *DashboardCompatibilityScore) 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 *DashboardCompatibilityScore) 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 *DashboardCompatibilityScore) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *DashboardCompatibilityScore) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *DashboardCompatibilityScore) 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 *DashboardCompatibilityScore) 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 *DashboardCompatibilityScore) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *DashboardCompatibilityScore) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *DashboardCompatibilityScore) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *DashboardCompatibilityScore) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *DashboardCompatibilityScore) DeepCopy() *DashboardCompatibilityScore { + cpy := &DashboardCompatibilityScore{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *DashboardCompatibilityScore) DeepCopyInto(dst *DashboardCompatibilityScore) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &DashboardCompatibilityScore{} + +// +k8s:openapi-gen=true +type DashboardCompatibilityScoreList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []DashboardCompatibilityScore `json:"items" yaml:"items"` +} + +func (o *DashboardCompatibilityScoreList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *DashboardCompatibilityScoreList) Copy() resource.ListObject { + cpy := &DashboardCompatibilityScoreList{ + TypeMeta: o.TypeMeta, + 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().(*DashboardCompatibilityScore); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +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] + } + return 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].(*DashboardCompatibilityScore) + } +} + +func (o *DashboardCompatibilityScoreList) DeepCopy() *DashboardCompatibilityScoreList { + cpy := &DashboardCompatibilityScoreList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *DashboardCompatibilityScoreList) DeepCopyInto(dst *DashboardCompatibilityScoreList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +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{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *Spec) DeepCopyInto(dst *Spec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of Status +func (s *Status) DeepCopy() *Status { + cpy := &Status{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Status into another Status object +func (s *Status) DeepCopyInto(dst *Status) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_schema_gen.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_schema_gen.go new file mode 100644 index 00000000000..080a7e0b489 --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_schema_gen.go @@ -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 diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_spec_gen.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_spec_gen.go new file mode 100644 index 00000000000..f363fecbe53 --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_spec_gen.go @@ -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{}, + } +} diff --git a/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_status_gen.go b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_status_gen.go new file mode 100644 index 00000000000..126ef1418e9 --- /dev/null +++ b/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_status_gen.go @@ -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" +) diff --git a/apps/dashvalidator/pkg/generated/manifestdata/dashvalidator_manifest.go b/apps/dashvalidator/pkg/generated/manifestdata/dashvalidator_manifest.go new file mode 100644 index 00000000000..c302943c8a2 --- /dev/null +++ b/apps/dashvalidator/pkg/generated/manifestdata/dashvalidator_manifest.go @@ -0,0 +1,124 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package manifestdata + +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" + + dashboardcompatibilityscorev1alpha1 "github.com/grafana/grafana/apps/dashvalidator/pkg/generated/dashboardcompatibilityscore/v1alpha1" +) + +var ( + rawSchemaDashboardCompatibilityScorev1alpha1 = []byte(`{"DashboardCompatibilityScore":{"properties":{"spec":{"$ref":"#/components/schemas/spec"},"status":{"$ref":"#/components/schemas/status"}},"required":["spec"]},"DataSourceMapping":{"additionalProperties":false,"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"},"DataSourceResult":{"additionalProperties":false,"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":{"$ref":"#/components/schemas/QueryBreakdown"},"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"},"OperatorState":{"additionalProperties":false,"properties":{"descriptiveState":{"description":"descriptiveState is an optional more descriptive state field which has no requirements on format","type":"string"},"details":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"details contains any extra information that is operator-specific","type":"object"},"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"},"QueryBreakdown":{"additionalProperties":false,"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"},"spec":{"additionalProperties":false,"properties":{"dashboardJson":{"additionalProperties":{"additionalProperties":{},"type":"object"},"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"},"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":{"$ref":"#/components/schemas/DataSourceMapping"},"type":"array"}},"required":["dashboardJson","datasourceMappings"],"type":"object"},"status":{"additionalProperties":false,"properties":{"additionalFields":{"additionalProperties":{"additionalProperties":{},"type":"object"},"description":"additionalFields is reserved for future use","type":"object"},"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":{"$ref":"#/components/schemas/DataSourceResult"},"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":{"$ref":"#/components/schemas/OperatorState"},"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"}}`) + versionSchemaDashboardCompatibilityScorev1alpha1 app.VersionSchema + _ = json.Unmarshal(rawSchemaDashboardCompatibilityScorev1alpha1, &versionSchemaDashboardCompatibilityScorev1alpha1) +) + +var appManifestData = app.ManifestData{ + AppName: "dashvalidator", + Group: "dashvalidator.ext.grafana.com", + PreferredVersion: "v1alpha1", + Versions: []app.ManifestVersion{ + { + Name: "v1alpha1", + Served: true, + Kinds: []app.ManifestVersionKind{ + { + Kind: "DashboardCompatibilityScore", + Plural: "DashboardCompatibilityScores", + Scope: "Namespaced", + Conversion: false, + Schema: &versionSchemaDashboardCompatibilityScorev1alpha1, + }, + }, + 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("dashvalidator") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "DashboardCompatibilityScore/v1alpha1": dashboardcompatibilityscorev1alpha1.Kind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} + +var customRouteToGoResponseType = map[string]any{} + +// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists. +// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths. +// If there is no association for the provided kind, version, custom route path, and method, exists will return false. +// Resource routes (those without a kind) should prefix their route with "/" if the route is namespaced (otherwise the route is assumed to be cluster-scope) +func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoParamsType = map[string]runtime.Object{} + +func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +var customRouteToGoRequestBodyType = map[string]any{} + +func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) { + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } + goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))] + return goType, exists +} + +type GoTypeAssociator struct{} + +func NewGoTypeAssociator() *GoTypeAssociator { + return &GoTypeAssociator{} +} + +func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) { + return ManifestGoTypeAssociator(kind, version) +} +func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteResponsesAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) { + return ManifestCustomRouteQueryAssociator(kind, version, path, verb) +} +func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) { + return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb) +} diff --git a/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.ts b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.ts new file mode 100644 index 00000000000..51b35cbec82 --- /dev/null +++ b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/dashboardcompatibilityscore_object_gen.ts @@ -0,0 +1,49 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; +import { Status } from './types.status.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface DashboardCompatibilityScore { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; + status: Status; +} diff --git a/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.metadata.gen.ts b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// 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. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.spec.gen.ts b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..e0480918d11 --- /dev/null +++ b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.spec.gen.ts @@ -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; + // 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: [], +}); + diff --git a/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.status.gen.ts b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.status.gen.ts new file mode 100644 index 00000000000..fbf341703ab --- /dev/null +++ b/apps/dashvalidator/plugin/src/generated/dashboardcompatibilityscore/v1alpha1/types.status.gen.ts @@ -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; +} + +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; + // Human-readable summary of validation result. + // Examples: "All queries compatible", "3 missing metrics found" + message?: string; + // additionalFields is reserved for future use + additionalFields?: Record; +} + +export const defaultStatus = (): Status => ({ + compatibilityScore: 0, + datasourceResults: [], +}); +