Create dashboard validator app
- Generate scaffolding - Create DashboardCompatibilityScore Kind in CUE
This commit is contained in:
@@ -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 $@
|
||||
+228
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/grafana/grafana/apps/dashvalidator
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
module: "github.com/grafana/grafana/apps/dashvalidator/kinds"
|
||||
language: version: "v0.8.2"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<integer>" or
|
||||
// "v<integer>(alpha|beta)<integer>". Each version contains the kinds your app manages for that version.
|
||||
// If your app needs access to kinds managed by another app, use permissions.accessKinds to allow your app access.
|
||||
versions: {
|
||||
"v1alpha1": v1alpha1
|
||||
}
|
||||
// extraPermissions contains any additional permissions your app may require to function.
|
||||
// Your app will always have all permissions for each kind it manages (the items defined in 'kinds').
|
||||
extraPermissions: {
|
||||
// If your app needs access to additional kinds supplied by other apps, you can list them here
|
||||
accessKinds: [
|
||||
// Here is an example for your app accessing the playlist kind for reads and watch
|
||||
// {
|
||||
// group: "playlist.grafana.app"
|
||||
// resource: "playlists"
|
||||
// actions: ["get","list","watch"]
|
||||
// }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// v1alpha1 is the v1alpha1 version of the app's API.
|
||||
// It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version.
|
||||
v1alpha1: {
|
||||
// kinds is the list of kinds served by this version
|
||||
kinds: [dashboardcompatibilityscorev0alpha1]
|
||||
// [OPTIONAL]
|
||||
// served indicates whether this particular version is served by the API server.
|
||||
// served should be set to false before a version is removed from the manifest entirely.
|
||||
// served defaults to true if not present.
|
||||
served: true
|
||||
// [OPTIONAL]
|
||||
// Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind.
|
||||
// If not present, default values within the codegen trait are used.
|
||||
// If you wish to specify codegen per-version, put this section in the version's object
|
||||
// (for example, <no value>v1alpha1) instead.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Executable
+43
@@ -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
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eufo pipefail
|
||||
|
||||
CLUSTER_NAME="dashvalidator"
|
||||
IMAGE="$1"
|
||||
|
||||
if [[ "$IMAGE" == "" ]]; then
|
||||
echo "usage: push_image.sh <image_name:tag>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
k3d image import "${IMAGE}" -c "${CLUSTER_NAME}"
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
+99
@@ -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)
|
||||
}
|
||||
+28
@@ -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{}
|
||||
+31
@@ -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{},
|
||||
}
|
||||
}
|
||||
+326
@@ -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)
|
||||
}
|
||||
+34
@@ -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
|
||||
+48
@@ -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{},
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// DataSourceResult contains validation results for a single datasource.
|
||||
// Provides aggregate statistics and per-query breakdown of compatibility.
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceResult struct {
|
||||
// Datasource UID that was validated (matches DataSourceMapping.uid)
|
||||
Uid string `json:"uid"`
|
||||
// Datasource type (matches DataSourceMapping.type)
|
||||
Type string `json:"type"`
|
||||
// Optional display name (matches DataSourceMapping.name if provided)
|
||||
Name *string `json:"name,omitempty"`
|
||||
// Total number of queries in the dashboard targeting this datasource.
|
||||
// Includes all panel targets/queries that reference this datasource.
|
||||
TotalQueries int64 `json:"totalQueries"`
|
||||
// Number of queries successfully validated.
|
||||
// May be less than totalQueries if some queries couldn't be parsed.
|
||||
CheckedQueries int64 `json:"checkedQueries"`
|
||||
// Total number of unique metrics/identifiers referenced across all queries.
|
||||
// For Prometheus: metric names extracted from PromQL expressions.
|
||||
// For SQL datasources: table and column names.
|
||||
TotalMetrics int64 `json:"totalMetrics"`
|
||||
// Number of metrics that exist in the datasource schema.
|
||||
// foundMetrics <= totalMetrics
|
||||
FoundMetrics int64 `json:"foundMetrics"`
|
||||
// Array of metric names that were referenced but don't exist.
|
||||
// Useful for debugging why a dashboard shows "no data".
|
||||
// Example for Prometheus: ["http_requests_total", "api_latency_seconds"]
|
||||
MissingMetrics []string `json:"missingMetrics"`
|
||||
// Per-query breakdown showing which specific queries have issues.
|
||||
// One entry per query target (refId: "A", "B", "C", etc.) in each panel.
|
||||
// Allows pinpointing exactly which panel/query needs fixing.
|
||||
QueryBreakdown []QueryBreakdown `json:"queryBreakdown"`
|
||||
// Overall compatibility score for this datasource (0-100).
|
||||
// Calculated as: (foundMetrics / totalMetrics) * 100
|
||||
// Used to calculate the global compatibilityScore in status.
|
||||
CompatibilityScore float64 `json:"compatibilityScore"`
|
||||
}
|
||||
|
||||
// NewDataSourceResult creates a new DataSourceResult object.
|
||||
func NewDataSourceResult() *DataSourceResult {
|
||||
return &DataSourceResult{
|
||||
MissingMetrics: []string{},
|
||||
QueryBreakdown: []QueryBreakdown{},
|
||||
}
|
||||
}
|
||||
|
||||
// QueryBreakdown provides compatibility details for a single query within a panel.
|
||||
// Granular per-query results allow users to identify exactly which queries need fixing.
|
||||
//
|
||||
// Note: A panel can have multiple queries (refId: "A", "B", "C", etc.),
|
||||
// so there may be multiple QueryBreakdown entries for the same panelID.
|
||||
// +k8s:openapi-gen=true
|
||||
type QueryBreakdown struct {
|
||||
// Human-readable panel title for context.
|
||||
// Example: "CPU Usage", "Request Rate"
|
||||
PanelTitle string `json:"panelTitle"`
|
||||
// Numeric panel ID from dashboard JSON.
|
||||
// Used to correlate with dashboard structure.
|
||||
PanelID int64 `json:"panelID"`
|
||||
// Query identifier within the panel.
|
||||
// Values: "A", "B", "C", etc. (from panel.targets[].refId)
|
||||
// Uniquely identifies which query in a multi-query panel this refers to.
|
||||
QueryRefId string `json:"queryRefId"`
|
||||
// Number of unique metrics referenced in this specific query.
|
||||
// For Prometheus: metrics extracted from the PromQL expr.
|
||||
// Example: rate(http_requests_total[5m]) references 1 metric.
|
||||
TotalMetrics int64 `json:"totalMetrics"`
|
||||
// Number of those metrics that exist in the datasource.
|
||||
// foundMetrics <= totalMetrics
|
||||
FoundMetrics int64 `json:"foundMetrics"`
|
||||
// Array of missing metric names specific to this query.
|
||||
// Helps identify exactly which part of a query expression will fail.
|
||||
// Empty array means query is fully compatible.
|
||||
MissingMetrics []string `json:"missingMetrics"`
|
||||
// Compatibility percentage for this individual query (0-100).
|
||||
// Calculated as: (foundMetrics / totalMetrics) * 100
|
||||
// 100 = query will work perfectly, 0 = query will return no data.
|
||||
CompatibilityScore float64 `json:"compatibilityScore"`
|
||||
}
|
||||
|
||||
// NewQueryBreakdown creates a new QueryBreakdown object.
|
||||
func NewQueryBreakdown() *QueryBreakdown {
|
||||
return &QueryBreakdown{
|
||||
MissingMetrics: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StatusOperatorState struct {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
LastEvaluation string `json:"lastEvaluation"`
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
State StatusOperatorStateState `json:"state"`
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
DescriptiveState *string `json:"descriptiveState,omitempty"`
|
||||
// details contains any extra information that is operator-specific
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// NewStatusOperatorState creates a new StatusOperatorState object.
|
||||
func NewStatusOperatorState() *StatusOperatorState {
|
||||
return &StatusOperatorState{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type Status struct {
|
||||
// Overall compatibility score across all datasources (0-100).
|
||||
// Calculated as: (total found metrics / total referenced metrics) * 100
|
||||
//
|
||||
// Score interpretation:
|
||||
// - 100: Perfect compatibility, all queries will work
|
||||
// - 80-99: Excellent, minor missing metrics
|
||||
// - 50-79: Fair, significant missing metrics
|
||||
// - 0-49: Poor, most queries will fail
|
||||
CompatibilityScore float64 `json:"compatibilityScore"`
|
||||
// Per-datasource validation results.
|
||||
// Array length matches spec.datasourceMappings.
|
||||
// Each element contains detailed metrics and query-level breakdown.
|
||||
DatasourceResults []DataSourceResult `json:"datasourceResults"`
|
||||
// ISO 8601 timestamp of when validation was last performed.
|
||||
// Example: "2024-01-15T10:30:00Z"
|
||||
LastChecked *string `json:"lastChecked,omitempty"`
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"`
|
||||
// Human-readable summary of validation result.
|
||||
// Examples: "All queries compatible", "3 missing metrics found"
|
||||
Message *string `json:"message,omitempty"`
|
||||
// additionalFields is reserved for future use
|
||||
AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"`
|
||||
}
|
||||
|
||||
// NewStatus creates a new Status object.
|
||||
func NewStatus() *Status {
|
||||
return &Status{
|
||||
DatasourceResults: []DataSourceResult{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type StatusOperatorStateState string
|
||||
|
||||
const (
|
||||
StatusOperatorStateStateSuccess StatusOperatorStateState = "success"
|
||||
StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress"
|
||||
StatusOperatorStateStateFailed StatusOperatorStateState = "failed"
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
+49
@@ -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<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
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;
|
||||
}
|
||||
Generated
+30
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
export const defaultMetadata = (): Metadata => ({
|
||||
updateTimestamp: "",
|
||||
createdBy: "",
|
||||
uid: "",
|
||||
creationTimestamp: "",
|
||||
finalizers: [],
|
||||
resourceVersion: "",
|
||||
generation: 0,
|
||||
updatedBy: "",
|
||||
labels: {},
|
||||
});
|
||||
|
||||
Generated
+42
@@ -0,0 +1,42 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// DataSourceMapping specifies a datasource to validate dashboard queries against.
|
||||
// Maps logical datasource references in the dashboard to actual datasource instances.
|
||||
export interface DataSourceMapping {
|
||||
// Unique identifier of the datasource instance.
|
||||
// Example: "prometheus-prod-us-west"
|
||||
uid: string;
|
||||
// Type of datasource plugin.
|
||||
// MVP: Only "prometheus" supported.
|
||||
// Future: "mysql", "postgres", "elasticsearch", etc.
|
||||
type: string;
|
||||
// Optional human-readable name for display in results.
|
||||
// If not provided, UID will be used in error messages.
|
||||
// Example: "Production Prometheus (US-West)"
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const defaultDataSourceMapping = (): DataSourceMapping => ({
|
||||
uid: "",
|
||||
type: "",
|
||||
});
|
||||
|
||||
export interface Spec {
|
||||
// Complete dashboard JSON object to validate.
|
||||
// Must be a v1 dashboard schema (contains "panels" array).
|
||||
// v2 dashboards (with "elements" structure) are not yet supported.
|
||||
dashboardJson: Record<string, any>;
|
||||
// Array of datasources to validate against.
|
||||
// The validator will check dashboard queries against each datasource
|
||||
// and provide per-datasource compatibility results.
|
||||
//
|
||||
// MVP: Only single datasource supported (array length = 1), Prometheus type only.
|
||||
// Future: Will support multiple datasources for dashboards with mixed queries.
|
||||
datasourceMappings: DataSourceMapping[];
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
dashboardJson: {},
|
||||
datasourceMappings: [],
|
||||
});
|
||||
|
||||
Generated
+142
@@ -0,0 +1,142 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// DataSourceResult contains validation results for a single datasource.
|
||||
// Provides aggregate statistics and per-query breakdown of compatibility.
|
||||
export interface DataSourceResult {
|
||||
// Datasource UID that was validated (matches DataSourceMapping.uid)
|
||||
uid: string;
|
||||
// Datasource type (matches DataSourceMapping.type)
|
||||
type: string;
|
||||
// Optional display name (matches DataSourceMapping.name if provided)
|
||||
name?: string;
|
||||
// Total number of queries in the dashboard targeting this datasource.
|
||||
// Includes all panel targets/queries that reference this datasource.
|
||||
totalQueries: number;
|
||||
// Number of queries successfully validated.
|
||||
// May be less than totalQueries if some queries couldn't be parsed.
|
||||
checkedQueries: number;
|
||||
// Total number of unique metrics/identifiers referenced across all queries.
|
||||
// For Prometheus: metric names extracted from PromQL expressions.
|
||||
// For SQL datasources: table and column names.
|
||||
totalMetrics: number;
|
||||
// Number of metrics that exist in the datasource schema.
|
||||
// foundMetrics <= totalMetrics
|
||||
foundMetrics: number;
|
||||
// Array of metric names that were referenced but don't exist.
|
||||
// Useful for debugging why a dashboard shows "no data".
|
||||
// Example for Prometheus: ["http_requests_total", "api_latency_seconds"]
|
||||
missingMetrics: string[];
|
||||
// Per-query breakdown showing which specific queries have issues.
|
||||
// One entry per query target (refId: "A", "B", "C", etc.) in each panel.
|
||||
// Allows pinpointing exactly which panel/query needs fixing.
|
||||
queryBreakdown: QueryBreakdown[];
|
||||
// Overall compatibility score for this datasource (0-100).
|
||||
// Calculated as: (foundMetrics / totalMetrics) * 100
|
||||
// Used to calculate the global compatibilityScore in status.
|
||||
compatibilityScore: number;
|
||||
}
|
||||
|
||||
export const defaultDataSourceResult = (): DataSourceResult => ({
|
||||
uid: "",
|
||||
type: "",
|
||||
totalQueries: 0,
|
||||
checkedQueries: 0,
|
||||
totalMetrics: 0,
|
||||
foundMetrics: 0,
|
||||
missingMetrics: [],
|
||||
queryBreakdown: [],
|
||||
compatibilityScore: 0,
|
||||
});
|
||||
|
||||
// QueryBreakdown provides compatibility details for a single query within a panel.
|
||||
// Granular per-query results allow users to identify exactly which queries need fixing.
|
||||
//
|
||||
// Note: A panel can have multiple queries (refId: "A", "B", "C", etc.),
|
||||
// so there may be multiple QueryBreakdown entries for the same panelID.
|
||||
export interface QueryBreakdown {
|
||||
// Human-readable panel title for context.
|
||||
// Example: "CPU Usage", "Request Rate"
|
||||
panelTitle: string;
|
||||
// Numeric panel ID from dashboard JSON.
|
||||
// Used to correlate with dashboard structure.
|
||||
panelID: number;
|
||||
// Query identifier within the panel.
|
||||
// Values: "A", "B", "C", etc. (from panel.targets[].refId)
|
||||
// Uniquely identifies which query in a multi-query panel this refers to.
|
||||
queryRefId: string;
|
||||
// Number of unique metrics referenced in this specific query.
|
||||
// For Prometheus: metrics extracted from the PromQL expr.
|
||||
// Example: rate(http_requests_total[5m]) references 1 metric.
|
||||
totalMetrics: number;
|
||||
// Number of those metrics that exist in the datasource.
|
||||
// foundMetrics <= totalMetrics
|
||||
foundMetrics: number;
|
||||
// Array of missing metric names specific to this query.
|
||||
// Helps identify exactly which part of a query expression will fail.
|
||||
// Empty array means query is fully compatible.
|
||||
missingMetrics: string[];
|
||||
// Compatibility percentage for this individual query (0-100).
|
||||
// Calculated as: (foundMetrics / totalMetrics) * 100
|
||||
// 100 = query will work perfectly, 0 = query will return no data.
|
||||
compatibilityScore: number;
|
||||
}
|
||||
|
||||
export const defaultQueryBreakdown = (): QueryBreakdown => ({
|
||||
panelTitle: "",
|
||||
panelID: 0,
|
||||
queryRefId: "",
|
||||
totalMetrics: 0,
|
||||
foundMetrics: 0,
|
||||
missingMetrics: [],
|
||||
compatibilityScore: 0,
|
||||
});
|
||||
|
||||
export interface OperatorState {
|
||||
// lastEvaluation is the ResourceVersion last evaluated
|
||||
lastEvaluation: string;
|
||||
// state describes the state of the lastEvaluation.
|
||||
// It is limited to three possible states for machine evaluation.
|
||||
state: "success" | "in_progress" | "failed";
|
||||
// descriptiveState is an optional more descriptive state field which has no requirements on format
|
||||
descriptiveState?: string;
|
||||
// details contains any extra information that is operator-specific
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultOperatorState = (): OperatorState => ({
|
||||
lastEvaluation: "",
|
||||
state: "success",
|
||||
});
|
||||
|
||||
export interface Status {
|
||||
// Overall compatibility score across all datasources (0-100).
|
||||
// Calculated as: (total found metrics / total referenced metrics) * 100
|
||||
//
|
||||
// Score interpretation:
|
||||
// - 100: Perfect compatibility, all queries will work
|
||||
// - 80-99: Excellent, minor missing metrics
|
||||
// - 50-79: Fair, significant missing metrics
|
||||
// - 0-49: Poor, most queries will fail
|
||||
compatibilityScore: number;
|
||||
// Per-datasource validation results.
|
||||
// Array length matches spec.datasourceMappings.
|
||||
// Each element contains detailed metrics and query-level breakdown.
|
||||
datasourceResults: DataSourceResult[];
|
||||
// ISO 8601 timestamp of when validation was last performed.
|
||||
// Example: "2024-01-15T10:30:00Z"
|
||||
lastChecked?: string;
|
||||
// operatorStates is a map of operator ID to operator state evaluations.
|
||||
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
|
||||
operatorStates?: Record<string, OperatorState>;
|
||||
// Human-readable summary of validation result.
|
||||
// Examples: "All queries compatible", "3 missing metrics found"
|
||||
message?: string;
|
||||
// additionalFields is reserved for future use
|
||||
additionalFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export const defaultStatus = (): Status => ({
|
||||
compatibilityScore: 0,
|
||||
datasourceResults: [],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user