Handle datasource variables in validator query grouping
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DashboardCompatibilityRequest contains the dashboard and datasource mappings to validate
|
||||
@@ -44,6 +45,13 @@ type DatasourceValidationResult struct {
|
||||
// ValidateDashboardCompatibility is the main entry point for validating dashboard compatibility
|
||||
// It extracts queries from the dashboard, validates them against each datasource, and returns aggregated results
|
||||
func ValidateDashboardCompatibility(ctx context.Context, req DashboardCompatibilityRequest) (*DashboardCompatibilityResult, error) {
|
||||
// MVP: Only support single datasource validation
|
||||
if len(req.DatasourceMappings) != 1 {
|
||||
return nil, fmt.Errorf("MVP only supports single datasource validation, got %d datasources", len(req.DatasourceMappings))
|
||||
}
|
||||
|
||||
singleDatasource := req.DatasourceMappings[0]
|
||||
|
||||
result := &DashboardCompatibilityResult{
|
||||
DatasourceResults: make([]DatasourceValidationResult, 0, len(req.DatasourceMappings)),
|
||||
}
|
||||
@@ -59,8 +67,8 @@ func ValidateDashboardCompatibility(ctx context.Context, req DashboardCompatibil
|
||||
fmt.Printf("[DEBUG] Query %d: DS=%s, RefID=%s, Query=%s\n", i, q.DatasourceUID, q.RefID, q.QueryText)
|
||||
}
|
||||
|
||||
// Step 2: Group queries by datasource UID
|
||||
queriesByDatasource := groupQueriesByDatasource(queries)
|
||||
// Step 2: Group queries by datasource UID (with variable resolution for MVP)
|
||||
queriesByDatasource := groupQueriesByDatasource(queries, singleDatasource.UID, req.DashboardJSON)
|
||||
|
||||
fmt.Printf("[DEBUG] Grouped queries by %d datasources\n", len(queriesByDatasource))
|
||||
for dsUID, dsQueries := range queriesByDatasource {
|
||||
@@ -287,6 +295,186 @@ func getDatasourceUIDFromValue(ds interface{}) string {
|
||||
}
|
||||
}
|
||||
|
||||
// isVariableReference checks if a string is a template variable reference
|
||||
// Matches patterns: ${varname}, $varname, [[varname]]
|
||||
// Follows Grafana's frontend regex: /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g
|
||||
// where \w = [A-Za-z0-9_] (alphanumeric + underscore, NO dashes)
|
||||
func isVariableReference(uid string) bool {
|
||||
if uid == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Match ${...} pattern - requires at least one \w character inside braces
|
||||
if len(uid) > 3 && uid[0] == '$' && uid[1] == '{' && uid[len(uid)-1] == '}' {
|
||||
// Extract content between ${ and }
|
||||
content := uid[2 : len(uid)-1]
|
||||
if len(content) == 0 {
|
||||
return false // Empty braces ${} not allowed
|
||||
}
|
||||
// Check if content starts with \w+ (before any . or :)
|
||||
for i, ch := range content {
|
||||
if ch == '.' || ch == ':' {
|
||||
// Found delimiter, check if we had at least one \w before it
|
||||
return i > 0
|
||||
}
|
||||
// Must be alphanumeric or underscore
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= '0' && ch <= '9') || ch == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true // All characters were valid \w
|
||||
}
|
||||
|
||||
// Match $varname pattern - requires at least one \w character after $
|
||||
// \w = alphanumeric + underscore (digits ARE allowed, dashes are NOT)
|
||||
if uid[0] == '$' && len(uid) > 1 {
|
||||
for i := 1; i < len(uid); i++ {
|
||||
ch := uid[i]
|
||||
// \w = [A-Za-z0-9_] only (NO dashes)
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= '0' && ch <= '9') || ch == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Match [[varname]] pattern - requires at least one \w character inside brackets
|
||||
// Also supports [[varname:format]] syntax
|
||||
if len(uid) > 4 && uid[0] == '[' && uid[1] == '[' &&
|
||||
uid[len(uid)-2] == ']' && uid[len(uid)-1] == ']' {
|
||||
// Extract content between [[ and ]]
|
||||
content := uid[2 : len(uid)-2]
|
||||
if len(content) == 0 {
|
||||
return false // Empty brackets [[]] not allowed
|
||||
}
|
||||
// Check if content starts with \w+ (before any :)
|
||||
for i, ch := range content {
|
||||
if ch == ':' {
|
||||
// Found format delimiter, check if we had at least one \w before it
|
||||
return i > 0
|
||||
}
|
||||
// Must be alphanumeric or underscore
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= '0' && ch <= '9') || ch == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true // All characters were valid \w
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// extractVariableName extracts the variable name from a variable reference
|
||||
// Returns only the name part, excluding fieldPath (after .) and format (after :)
|
||||
// Examples: ${var.field} -> "var", [[var:text]] -> "var", $datasource -> "datasource"
|
||||
func extractVariableName(varRef string) string {
|
||||
if !isVariableReference(varRef) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Handle ${varname} pattern - may include .fieldPath or :format
|
||||
if len(varRef) > 3 && varRef[0] == '$' && varRef[1] == '{' && varRef[len(varRef)-1] == '}' {
|
||||
content := varRef[2 : len(varRef)-1]
|
||||
// Extract only up to . or :
|
||||
for i, ch := range content {
|
||||
if ch == '.' || ch == ':' {
|
||||
return content[:i]
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// Handle $varname pattern - no modifiers possible
|
||||
if varRef[0] == '$' && len(varRef) > 1 {
|
||||
return varRef[1:]
|
||||
}
|
||||
|
||||
// Handle [[varname]] pattern - may include :format
|
||||
if len(varRef) > 4 && varRef[0] == '[' && varRef[1] == '[' {
|
||||
content := varRef[2 : len(varRef)-2]
|
||||
// Extract only up to :
|
||||
for i, ch := range content {
|
||||
if ch == ':' {
|
||||
return content[:i]
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isPrometheusVariable checks if a variable reference points to a Prometheus datasource
|
||||
// Looks in dashboard.__inputs for the datasource type
|
||||
func isPrometheusVariable(varRef string, dashboardJSON map[string]interface{}) bool {
|
||||
if !isVariableReference(varRef) {
|
||||
return false
|
||||
}
|
||||
|
||||
varName := extractVariableName(varRef)
|
||||
if varName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for __inputs array in dashboard
|
||||
inputs, hasInputs := dashboardJSON["__inputs"].([]interface{})
|
||||
if !hasInputs {
|
||||
// No __inputs, assume it might be Prometheus (MVP: single datasource)
|
||||
// This is a fallback for dashboards without explicit __inputs
|
||||
return true
|
||||
}
|
||||
|
||||
// Search for this variable in __inputs
|
||||
for _, inputInterface := range inputs {
|
||||
input, ok := inputInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this input matches our variable name
|
||||
inputName := getStringValue(input, "name", "")
|
||||
inputType := getStringValue(input, "type", "")
|
||||
inputPluginID := getStringValue(input, "pluginId", "")
|
||||
|
||||
// Match by name (case-insensitive for flexibility)
|
||||
if inputName != "" && varName != "" {
|
||||
if inputName == varName ||
|
||||
strings.EqualFold(inputName, varName) ||
|
||||
strings.Contains(strings.ToLower(varName), strings.ToLower(inputName)) {
|
||||
// Check if it's a datasource input with prometheus plugin
|
||||
if inputType == "datasource" && inputPluginID == "prometheus" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found or not Prometheus
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveDatasourceUID resolves a datasource UID, handling variable references (MVP: single datasource)
|
||||
// For MVP, all Prometheus variables resolve to the single datasource UID
|
||||
func resolveDatasourceUID(uid string, singleDatasourceUID string, dashboardJSON map[string]interface{}) string {
|
||||
// If not a variable, return as-is (concrete UID)
|
||||
if !isVariableReference(uid) {
|
||||
return uid
|
||||
}
|
||||
|
||||
// Check if it's a Prometheus variable
|
||||
if isPrometheusVariable(uid, dashboardJSON) {
|
||||
fmt.Printf("[DEBUG] Resolved Prometheus variable %s to %s\n", uid, singleDatasourceUID)
|
||||
return singleDatasourceUID
|
||||
}
|
||||
|
||||
// Non-Prometheus variable, return as-is (will be ignored in grouping)
|
||||
fmt.Printf("[DEBUG] Variable %s is not a Prometheus variable, skipping\n", uid)
|
||||
return uid
|
||||
}
|
||||
|
||||
// extractQueryText extracts the query text from a target
|
||||
// Different datasources use different field names (expr, query, rawSql, etc.)
|
||||
func extractQueryText(target map[string]interface{}) string {
|
||||
@@ -337,7 +525,8 @@ type DashboardQuery struct {
|
||||
}
|
||||
|
||||
// groupQueriesByDatasource groups dashboard queries by their datasource UID
|
||||
func groupQueriesByDatasource(queries []DashboardQuery) map[string][]Query {
|
||||
// For MVP: resolves Prometheus template variables to the single datasource UID
|
||||
func groupQueriesByDatasource(queries []DashboardQuery, singleDatasourceUID string, dashboardJSON map[string]interface{}) map[string][]Query {
|
||||
grouped := make(map[string][]Query)
|
||||
|
||||
for _, dq := range queries {
|
||||
@@ -348,7 +537,13 @@ func groupQueriesByDatasource(queries []DashboardQuery) map[string][]Query {
|
||||
PanelID: dq.PanelID,
|
||||
}
|
||||
|
||||
grouped[dq.DatasourceUID] = append(grouped[dq.DatasourceUID], q)
|
||||
// Resolve datasource UID (handles both concrete UIDs and variables)
|
||||
resolvedUID := resolveDatasourceUID(dq.DatasourceUID, singleDatasourceUID, dashboardJSON)
|
||||
|
||||
// Only add to grouping if we got a valid resolved UID
|
||||
if resolvedUID != "" {
|
||||
grouped[resolvedUID] = append(grouped[resolvedUID], q)
|
||||
}
|
||||
}
|
||||
|
||||
return grouped
|
||||
|
||||
Reference in New Issue
Block a user