diff --git a/apps/dashvalidator/go.mod b/apps/dashvalidator/go.mod index a90f960f3c3..0470c3e38a7 100644 --- a/apps/dashvalidator/go.mod +++ b/apps/dashvalidator/go.mod @@ -6,6 +6,7 @@ require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 github.com/grafana/grafana-app-sdk v0.48.7 github.com/grafana/grafana-app-sdk/logging v0.48.7 + github.com/prometheus/prometheus v0.303.1 k8s.io/apimachinery v0.34.3 k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e ) @@ -38,6 +39,7 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect github.com/diegoholiveira/jsonlogic/v3 v3.7.4 // indirect github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -91,6 +93,7 @@ require ( github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grafana/sqlds/v4 v4.2.7 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect diff --git a/apps/dashvalidator/go.sum b/apps/dashvalidator/go.sum index abbde7a2bb3..97ead7b33d2 100644 --- a/apps/dashvalidator/go.sum +++ b/apps/dashvalidator/go.sum @@ -1,6 +1,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= +cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cuelang.org/go v0.11.1 h1:pV+49MX1mmvDm8Qh3Za3M786cty8VKPWzQ1Ho4gZRP0= cuelang.org/go v0.11.1/go.mod h1:PBY6XvPUswPPJ2inpvUozP9mebDVTXaeehQikhZPBz0= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -84,6 +91,8 @@ github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -141,6 +150,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= @@ -334,11 +345,17 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -370,6 +387,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA= github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= @@ -554,6 +573,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -627,6 +648,10 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/prometheus v0.303.1 h1:He/2jRE6sB23Ew38AIoR1WRR3fCMgPlJA2E0obD2WSY= +github.com/prometheus/prometheus v0.303.1/go.mod h1:WEq2ogBPZoLjj9x5K67VEk7ECR0nRD9XCjaOt1lsYck= +github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= +github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -872,6 +897,8 @@ gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= +google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/apps/dashvalidator/pkg/app/app.go b/apps/dashvalidator/pkg/app/app.go index 7812b41d405..2ea93df4ffb 100644 --- a/apps/dashvalidator/pkg/app/app.go +++ b/apps/dashvalidator/pkg/app/app.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "github.com/grafana/grafana-app-sdk/app" @@ -13,13 +14,61 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" validatorv1alpha1 "github.com/grafana/grafana/apps/dashvalidator/pkg/apis/dashvalidator/v1alpha1" + "github.com/grafana/grafana/apps/dashvalidator/pkg/validator" + _ "github.com/grafana/grafana/apps/dashvalidator/pkg/validator/prometheus" // Register prometheus validator via init() + "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" ) type DashValidatorConfig struct { - DatasourceSvc datasources.DataSourceService - PluginCtx *plugincontext.Provider + DatasourceSvc datasources.DataSourceService + PluginCtx *plugincontext.Provider + HTTPClientProvider httpclient.Provider +} + +// checkRequest matches the CUE schema for POST /check request +type checkRequest struct { + DashboardJSON map[string]interface{} `json:"dashboardJson"` + DatasourceMappings []datasourceMapping `json:"datasourceMappings"` +} + +// datasourceMapping represents a datasource to validate against +type datasourceMapping struct { + UID string `json:"uid"` + Type string `json:"type"` + Name *string `json:"name,omitempty"` +} + +// checkResponse matches the CUE schema for POST /check response +type checkResponse struct { + CompatibilityScore float64 `json:"compatibilityScore"` + DatasourceResults []datasourceResult `json:"datasourceResults"` +} + +// datasourceResult contains validation results for a single datasource +type datasourceResult struct { + UID string `json:"uid"` + Type string `json:"type"` + Name *string `json:"name,omitempty"` + TotalQueries int `json:"totalQueries"` + CheckedQueries int `json:"checkedQueries"` + TotalMetrics int `json:"totalMetrics"` + FoundMetrics int `json:"foundMetrics"` + MissingMetrics []string `json:"missingMetrics"` + QueryBreakdown []queryResult `json:"queryBreakdown"` + CompatibilityScore float64 `json:"compatibilityScore"` +} + +// queryResult contains validation results for a single query +type queryResult struct { + PanelTitle string `json:"panelTitle"` + PanelID int `json:"panelID"` + QueryRefID string `json:"queryRefId"` + TotalMetrics int `json:"totalMetrics"` + FoundMetrics int `json:"foundMetrics"` + MissingMetrics []string `json:"missingMetrics"` + CompatibilityScore float64 `json:"compatibilityScore"` } func New(cfg app.Config) (app.App, error) { @@ -42,7 +91,7 @@ func New(cfg app.Config) (app.App, error) { Namespaced: true, Path: "check", Method: "POST", - }: handleCheckRoute(log, specificConfig.DatasourceSvc, specificConfig.PluginCtx), + }: handleCheckRoute(log, specificConfig.DatasourceSvc, specificConfig.PluginCtx, specificConfig.HTTPClientProvider), }, }, } @@ -60,21 +109,149 @@ func handleCheckRoute( log logging.Logger, datasourceSvc datasources.DataSourceService, pluginCtx *plugincontext.Provider, + httpClientProvider httpclient.Provider, ) func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error { return func(ctx context.Context, w app.CustomRouteResponseWriter, r *app.CustomRouteRequest) error { logger := log.WithContext(ctx) logger.Info("Received compatibility check request") - // TODO validation logic here + // Step 1: Parse request body + body, err := io.ReadAll(r.Body) + if err != nil { + logger.Error("Failed to read request body", "error", err) + w.WriteHeader(http.StatusBadRequest) + return json.NewEncoder(w).Encode(map[string]string{ + "error": "failed to read request body", + }) + } - // for now a simple response + var req checkRequest + if err := json.Unmarshal(body, &req); err != nil { + logger.Error("Failed to parse request JSON", "error", err) + w.WriteHeader(http.StatusBadRequest) + return json.NewEncoder(w).Encode(map[string]string{ + "error": "invalid JSON in request body", + }) + } + // Step 2: Build validator request + validatorReq := validator.DashboardCompatibilityRequest{ + DashboardJSON: req.DashboardJSON, + DatasourceMappings: make([]validator.DatasourceMapping, 0, len(req.DatasourceMappings)), + } + + logger.Info("Processing request", "dashboardTitle", req.DashboardJSON["title"], "numMappings", len(req.DatasourceMappings)) + + // Get namespace from request (needed for datasource lookup) + namespace := r.ResourceIdentifier.Namespace + + for _, dsMapping := range req.DatasourceMappings { + // Convert optional name pointer to string + name := "" + if dsMapping.Name != nil { + name = *dsMapping.Name + } + + // Fetch datasource from Grafana using app-platform method + // Parameters: namespace, name (UID), group (datasource type) + ds, err := datasourceSvc.GetDataSourceInNamespace(ctx, namespace, dsMapping.UID, dsMapping.Type) + if err != nil { + logger.Error("Failed to get datasource", "namespace", namespace, "uid", dsMapping.UID, "type", dsMapping.Type, "error", err) + w.WriteHeader(http.StatusBadRequest) + return json.NewEncoder(w).Encode(map[string]string{ + "error": fmt.Sprintf("datasource not found: %s", dsMapping.UID), + }) + } + + logger.Info("Retrieved datasource", "uid", ds.UID, "url", ds.URL, "type", ds.Type) + + // Get authenticated HTTP transport for this datasource + transport, err := datasourceSvc.GetHTTPTransport(ctx, ds, httpClientProvider) + if err != nil { + logger.Error("Failed to get HTTP transport", "uid", ds.UID, "error", err) + w.WriteHeader(http.StatusInternalServerError) + return json.NewEncoder(w).Encode(map[string]string{ + "error": fmt.Sprintf("failed to configure datasource transport: %s", dsMapping.UID), + }) + } + + // Create HTTP client with authenticated transport + httpClient := &http.Client{ + Transport: transport, + } + + validatorReq.DatasourceMappings = append(validatorReq.DatasourceMappings, validator.DatasourceMapping{ + UID: dsMapping.UID, + Type: dsMapping.Type, + Name: name, + URL: ds.URL, + HTTPClient: httpClient, // Pass authenticated client + }) + } + + // Step 3: Validate dashboard compatibility + result, err := validator.ValidateDashboardCompatibility(ctx, validatorReq) + if err != nil { + logger.Error("Validation failed", "error", err) + w.WriteHeader(http.StatusInternalServerError) + return json.NewEncoder(w).Encode(map[string]string{ + "error": fmt.Sprintf("validation failed: %v", err), + }) + } + + // Step 4: Convert result to response format + response := convertToCheckResponse(result) + + // Step 5: Return response w.WriteHeader(http.StatusOK) - return json.NewEncoder(w).Encode(map[string]interface{}{ - "message": "Handler working!", + return json.NewEncoder(w).Encode(response) + } +} + +// convertToCheckResponse converts validator result to API response format +func convertToCheckResponse(result *validator.DashboardCompatibilityResult) checkResponse { + response := checkResponse{ + CompatibilityScore: result.CompatibilityScore, + DatasourceResults: make([]datasourceResult, 0, len(result.DatasourceResults)), + } + + for _, dsResult := range result.DatasourceResults { + // Convert name string to pointer + var name *string + if dsResult.Name != "" { + name = &dsResult.Name + } + + // Convert query results + queryBreakdown := make([]queryResult, 0, len(dsResult.QueryBreakdown)) + for _, qr := range dsResult.QueryBreakdown { + queryBreakdown = append(queryBreakdown, queryResult{ + PanelTitle: qr.PanelTitle, + PanelID: qr.PanelID, + QueryRefID: qr.QueryRefID, + TotalMetrics: qr.TotalMetrics, + FoundMetrics: qr.FoundMetrics, + MissingMetrics: qr.MissingMetrics, + CompatibilityScore: qr.CompatibilityScore, + }) + } + + response.DatasourceResults = append(response.DatasourceResults, datasourceResult{ + UID: dsResult.UID, + Type: dsResult.Type, + Name: name, + TotalQueries: dsResult.TotalQueries, + CheckedQueries: dsResult.CheckedQueries, + TotalMetrics: dsResult.TotalMetrics, + FoundMetrics: dsResult.FoundMetrics, + MissingMetrics: dsResult.MissingMetrics, + QueryBreakdown: queryBreakdown, + CompatibilityScore: dsResult.CompatibilityScore, }) } + + return response } func GetKinds() map[schema.GroupVersion][]resource.Kind { diff --git a/apps/dashvalidator/pkg/validator/dashboard.go b/apps/dashvalidator/pkg/validator/dashboard.go new file mode 100644 index 00000000000..6f3ccdf7809 --- /dev/null +++ b/apps/dashvalidator/pkg/validator/dashboard.go @@ -0,0 +1,354 @@ +package validator + +import ( + "context" + "fmt" + "net/http" +) + +// DashboardCompatibilityRequest contains the dashboard and datasource mappings to validate +type DashboardCompatibilityRequest struct { + DashboardJSON map[string]interface{} // Dashboard JSON structure + DatasourceMappings []DatasourceMapping // List of datasources to validate against +} + +// DatasourceMapping maps a datasource UID to its type and optionally name/URL +type DatasourceMapping struct { + UID string // Datasource UID + Type string // Datasource type (prometheus, mysql, etc.) + Name string // Optional: Datasource name + URL string // Datasource URL + HTTPClient *http.Client // Authenticated HTTP client +} + +// DashboardCompatibilityResult contains the validation results for a dashboard +type DashboardCompatibilityResult struct { + CompatibilityScore float64 // Overall compatibility (0.0 - 1.0) + DatasourceResults []DatasourceValidationResult // Per-datasource results +} + +// DatasourceValidationResult contains validation results for one datasource +type DatasourceValidationResult struct { + UID string + Type string + Name string + TotalQueries int + CheckedQueries int + TotalMetrics int + FoundMetrics int + MissingMetrics []string + QueryBreakdown []QueryResult + CompatibilityScore float64 +} + +// ValidateDashboardCompatibility is the main entry point for validating dashboard compatibility +// It extracts queries from the dashboard, validates them against each datasource, and returns aggregated results +func ValidateDashboardCompatibility(ctx context.Context, req DashboardCompatibilityRequest) (*DashboardCompatibilityResult, error) { + result := &DashboardCompatibilityResult{ + DatasourceResults: make([]DatasourceValidationResult, 0, len(req.DatasourceMappings)), + } + + // Step 1: Extract queries from dashboard JSON + queries, err := extractQueriesFromDashboard(req.DashboardJSON) + if err != nil { + return nil, fmt.Errorf("failed to extract queries from dashboard: %w", err) + } + + fmt.Printf("[DEBUG] Extracted %d queries from dashboard\n", len(queries)) + for i, q := range queries { + fmt.Printf("[DEBUG] Query %d: DS=%s, RefID=%s, Query=%s\n", i, q.DatasourceUID, q.RefID, q.QueryText) + } + + // Step 2: Group queries by datasource UID + queriesByDatasource := groupQueriesByDatasource(queries) + + fmt.Printf("[DEBUG] Grouped queries by %d datasources\n", len(queriesByDatasource)) + for dsUID, dsQueries := range queriesByDatasource { + fmt.Printf("[DEBUG] Datasource %s has %d queries\n", dsUID, len(dsQueries)) + } + + // Step 3: Validate each datasource + var totalCompatibility float64 + validatedCount := 0 + + for _, dsMapping := range req.DatasourceMappings { + fmt.Printf("[DEBUG] Processing datasource mapping: UID=%s, Type=%s, URL=%s\n", dsMapping.UID, dsMapping.Type, dsMapping.URL) + + // Get queries for this datasource + dsQueries, ok := queriesByDatasource[dsMapping.UID] + if !ok || len(dsQueries) == 0 { + // No queries for this datasource, skip + fmt.Printf("[DEBUG] No queries found for datasource %s, skipping\n", dsMapping.UID) + continue + } + + fmt.Printf("[DEBUG] Found %d queries for datasource %s\n", len(dsQueries), dsMapping.UID) + + // Get validator for this datasource type + v, err := GetValidator(dsMapping.Type) + if err != nil { + // Unsupported datasource type, skip but log + fmt.Printf("[DEBUG] Failed to get validator for type %s: %v\n", dsMapping.Type, err) + continue + } + + fmt.Printf("[DEBUG] Got validator for type %s, starting validation\n", dsMapping.Type) + + // Build Datasource struct + ds := Datasource{ + UID: dsMapping.UID, + Type: dsMapping.Type, + Name: dsMapping.Name, + URL: dsMapping.URL, + HTTPClient: dsMapping.HTTPClient, + } + + // Validate queries + validationResult, err := v.ValidateQueries(ctx, dsQueries, ds) + if err != nil { + // Validation failed for this datasource, skip but could log + continue + } + + // Convert to DatasourceValidationResult + dsResult := DatasourceValidationResult{ + UID: dsMapping.UID, + Type: dsMapping.Type, + Name: dsMapping.Name, + TotalQueries: validationResult.TotalQueries, + CheckedQueries: validationResult.CheckedQueries, + TotalMetrics: validationResult.TotalMetrics, + FoundMetrics: validationResult.FoundMetrics, + MissingMetrics: validationResult.MissingMetrics, + QueryBreakdown: validationResult.QueryBreakdown, + CompatibilityScore: validationResult.CompatibilityScore, + } + + result.DatasourceResults = append(result.DatasourceResults, dsResult) + totalCompatibility += validationResult.CompatibilityScore + validatedCount++ + } + + // Step 4: Calculate overall compatibility score + if validatedCount > 0 { + result.CompatibilityScore = totalCompatibility / float64(validatedCount) + } else { + result.CompatibilityScore = 1.0 // No datasources = perfect compatibility + } + + return result, nil +} + +// extractQueriesFromDashboard parses the dashboard JSON and extracts all queries +// Supports both v1 (legacy) and v2 (new) dashboard formats +func extractQueriesFromDashboard(dashboardJSON map[string]interface{}) ([]DashboardQuery, error) { + var queries []DashboardQuery + + // Debug: Print what keys we have + fmt.Printf("[DEBUG] Dashboard JSON keys: ") + for key := range dashboardJSON { + fmt.Printf("%s, ", key) + } + fmt.Printf("\n") + + // Detect dashboard version (v1 uses "panels", v2 uses different structure) + // For MVP, we only support v1 (legacy format with panels array) + if !isV1Dashboard(dashboardJSON) { + fmt.Printf("[DEBUG] isV1Dashboard returned false, 'panels' key exists: %v\n", dashboardJSON["panels"] != nil) + return nil, fmt.Errorf("unsupported dashboard format: only v1 dashboards are supported in MVP") + } + + // Extract panels array + panels, ok := dashboardJSON["panels"].([]interface{}) + if !ok { + // No panels in dashboard, return empty array + return queries, nil + } + + // Iterate through all panels + for _, panelInterface := range panels { + panel, ok := panelInterface.(map[string]interface{}) + if !ok { + continue + } + + // Extract queries from this panel + panelQueries := extractQueriesFromPanel(panel) + queries = append(queries, panelQueries...) + + // Handle nested panels in collapsed rows + nestedPanels, hasNested := panel["panels"].([]interface{}) + if hasNested { + for _, nestedPanelInterface := range nestedPanels { + nestedPanel, ok := nestedPanelInterface.(map[string]interface{}) + if !ok { + continue + } + nestedQueries := extractQueriesFromPanel(nestedPanel) + queries = append(queries, nestedQueries...) + } + } + } + + return queries, nil +} + +// isV1Dashboard checks if a dashboard is in v1 (legacy) format +// v1 dashboards have a "panels" array at the top level +func isV1Dashboard(dashboard map[string]interface{}) bool { + _, hasPanels := dashboard["panels"] + return hasPanels +} + +// extractQueriesFromPanel extracts all queries/targets from a single panel +func extractQueriesFromPanel(panel map[string]interface{}) []DashboardQuery { + var queries []DashboardQuery + + // Get panel info for context + panelTitle := getStringValue(panel, "title", "Untitled Panel") + panelID := getIntValue(panel, "id", 0) + + // Extract targets array (queries) + targets, hasTargets := panel["targets"].([]interface{}) + if !hasTargets { + return queries + } + + // Iterate through each target/query + for _, targetInterface := range targets { + target, ok := targetInterface.(map[string]interface{}) + if !ok { + continue + } + + // Extract datasource UID + datasourceUID := extractDatasourceUID(target, panel) + if datasourceUID == "" { + // Skip queries without datasource + continue + } + + // Extract query text (different fields for different datasources) + queryText := extractQueryText(target) + if queryText == "" { + // Skip empty queries + continue + } + + // Extract refId (A, B, C, etc.) + refID := getStringValue(target, "refId", "") + + // Build DashboardQuery + query := DashboardQuery{ + DatasourceUID: datasourceUID, + RefID: refID, + QueryText: queryText, + PanelTitle: panelTitle, + PanelID: panelID, + } + + queries = append(queries, query) + } + + return queries +} + +// extractDatasourceUID gets the datasource UID from a target, falling back to panel datasource +func extractDatasourceUID(target map[string]interface{}, panel map[string]interface{}) string { + // Try target-level datasource first + if ds, ok := target["datasource"]; ok { + if uid := getDatasourceUIDFromValue(ds); uid != "" { + return uid + } + } + + // Fall back to panel-level datasource + if ds, ok := panel["datasource"]; ok { + if uid := getDatasourceUIDFromValue(ds); uid != "" { + return uid + } + } + + return "" +} + +// getDatasourceUIDFromValue extracts UID from datasource value (can be string or object) +func getDatasourceUIDFromValue(ds interface{}) string { + switch v := ds.(type) { + case string: + // Direct UID string + return v + case map[string]interface{}: + // Structured datasource reference { uid: "...", type: "..." } + return getStringValue(v, "uid", "") + default: + return "" + } +} + +// extractQueryText extracts the query text from a target +// Different datasources use different field names (expr, query, rawSql, etc.) +func extractQueryText(target map[string]interface{}) string { + // Try common query field names + queryFields := []string{"expr", "query", "rawSql", "rawQuery", "target", "measurement"} + + for _, field := range queryFields { + if queryText := getStringValue(target, field, ""); queryText != "" { + return queryText + } + } + + return "" +} + +// getStringValue safely extracts a string value from a map +func getStringValue(m map[string]interface{}, key string, defaultValue string) string { + if value, ok := m[key]; ok { + if s, ok := value.(string); ok { + return s + } + } + return defaultValue +} + +// getIntValue safely extracts an int value from a map +func getIntValue(m map[string]interface{}, key string, defaultValue int) int { + if value, ok := m[key]; ok { + switch v := value.(type) { + case int: + return v + case float64: + return int(v) + case int64: + return int(v) + } + } + return defaultValue +} + +// DashboardQuery represents a query extracted from a dashboard panel +type DashboardQuery struct { + DatasourceUID string // Which datasource this query belongs to + RefID string // Query reference ID + QueryText string // The actual query + PanelTitle string // Panel title + PanelID int // Panel ID +} + +// groupQueriesByDatasource groups dashboard queries by their datasource UID +func groupQueriesByDatasource(queries []DashboardQuery) map[string][]Query { + grouped := make(map[string][]Query) + + for _, dq := range queries { + q := Query{ + RefID: dq.RefID, + QueryText: dq.QueryText, + PanelTitle: dq.PanelTitle, + PanelID: dq.PanelID, + } + + grouped[dq.DatasourceUID] = append(grouped[dq.DatasourceUID], q) + } + + return grouped +} diff --git a/apps/dashvalidator/pkg/validator/prometheus/fetcher.go b/apps/dashvalidator/pkg/validator/prometheus/fetcher.go new file mode 100644 index 00000000000..dfe908475dc --- /dev/null +++ b/apps/dashvalidator/pkg/validator/prometheus/fetcher.go @@ -0,0 +1,76 @@ +package prometheus + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +// Fetcher fetches available metrics from a Prometheus datasource +type Fetcher struct{} + +// NewFetcher creates a new Prometheus metrics fetcher +func NewFetcher() *Fetcher { + return &Fetcher{} +} + +// prometheusResponse represents the Prometheus API response structure +type prometheusResponse struct { + Status string `json:"status"` + Data []string `json:"data"` + Error string `json:"error,omitempty"` +} + +// FetchMetrics queries Prometheus to get all available metric names +// It uses the /api/v1/label/__name__/values endpoint +// The provided HTTP client should have proper authentication configured +func (f *Fetcher) FetchMetrics(ctx context.Context, datasourceURL string, client *http.Client) ([]string, error) { + // Build the API URL + baseURL, err := url.Parse(datasourceURL) + if err != nil { + return nil, fmt.Errorf("invalid datasource URL: %w", err) + } + + // Prometheus metrics endpoint + apiPath, err := url.Parse("/api/v1/label/__name__/values") + if err != nil { + return nil, fmt.Errorf("failed to parse API path: %w", err) + } + + fullURL := baseURL.ResolveReference(apiPath) + + // Create the request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Execute the request using the provided authenticated client + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch metrics from Prometheus: %w", err) + } + defer resp.Body.Close() + + // Check status code + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("Prometheus API returned status %d: %s", resp.StatusCode, string(body)) + } + + // Parse the response + var promResp prometheusResponse + if err := json.NewDecoder(resp.Body).Decode(&promResp); err != nil { + return nil, fmt.Errorf("failed to decode Prometheus response: %w", err) + } + + // Check Prometheus API status + if promResp.Status != "success" { + return nil, fmt.Errorf("Prometheus API returned error: %s", promResp.Error) + } + + return promResp.Data, nil +} diff --git a/apps/dashvalidator/pkg/validator/prometheus/parser.go b/apps/dashvalidator/pkg/validator/prometheus/parser.go new file mode 100644 index 00000000000..aa47b083de9 --- /dev/null +++ b/apps/dashvalidator/pkg/validator/prometheus/parser.go @@ -0,0 +1,49 @@ +package prometheus + +import ( + "fmt" + + "github.com/prometheus/prometheus/promql/parser" +) + +// Parser extracts metric names from PromQL queries +type Parser struct{} + +// NewParser creates a new PromQL parser +func NewParser() *Parser { + return &Parser{} +} + +// ExtractMetrics parses a PromQL query and extracts all metric names +// For example: "rate(http_requests_total[5m])" returns ["http_requests_total"] +func (p *Parser) ExtractMetrics(query string) ([]string, error) { + // Parse the PromQL expression + expr, err := parser.ParseExpr(query) + if err != nil { + return nil, fmt.Errorf("failed to parse PromQL query: %w", err) + } + + // Extract metric names by walking the AST + metrics := make(map[string]bool) // Use map to deduplicate + parser.Inspect(expr, func(node parser.Node, _ []parser.Node) error { + // VectorSelector represents a metric selector like "up" or "up{job="foo"}" + if vs, ok := node.(*parser.VectorSelector); ok { + metrics[vs.Name] = true + } + // MatrixSelector represents range queries like "up[5m]" + if ms, ok := node.(*parser.MatrixSelector); ok { + if vs, ok := ms.VectorSelector.(*parser.VectorSelector); ok { + metrics[vs.Name] = true + } + } + return nil + }) + + // Convert map to slice + result := make([]string, 0, len(metrics)) + for metric := range metrics { + result = append(result, metric) + } + + return result, nil +} diff --git a/apps/dashvalidator/pkg/validator/prometheus/validator.go b/apps/dashvalidator/pkg/validator/prometheus/validator.go new file mode 100644 index 00000000000..a3b368c4429 --- /dev/null +++ b/apps/dashvalidator/pkg/validator/prometheus/validator.go @@ -0,0 +1,149 @@ +package prometheus + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/apps/dashvalidator/pkg/validator" +) + +// Register Prometheus validator on package import +func init() { + validator.RegisterValidator("prometheus", func() validator.DatasourceValidator { + return NewValidator() + }) +} + +// Validator implements validator.DatasourceValidator for Prometheus datasources +type Validator struct { + parser *Parser + fetcher *Fetcher +} + +// NewValidator creates a new Prometheus validator +func NewValidator() validator.DatasourceValidator { + return &Validator{ + parser: NewParser(), + fetcher: NewFetcher(), + } +} + +// ValidateQueries validates Prometheus queries against the datasource +func (v *Validator) ValidateQueries(ctx context.Context, queries []validator.Query, datasource validator.Datasource) (*validator.ValidationResult, error) { + fmt.Printf("[DEBUG PROM] Starting validation for %d queries against datasource %s\n", len(queries), datasource.URL) + + result := &validator.ValidationResult{ + TotalQueries: len(queries), + QueryBreakdown: make([]validator.QueryResult, 0, len(queries)), + } + + // Step 1: Parse all queries to extract metrics + allMetrics := make(map[string]bool) // Use map to deduplicate + queryMetrics := make(map[int][]string) + + for i, query := range queries { + fmt.Printf("[DEBUG PROM] Parsing query %d: %s\n", i, query.QueryText) + metrics, err := v.parser.ExtractMetrics(query.QueryText) + if err != nil { + // If we can't parse the query, we still continue with others + // but we don't count this query as "checked" + fmt.Printf("[DEBUG PROM] Failed to parse query %d: %v\n", i, err) + continue + } + fmt.Printf("[DEBUG PROM] Extracted %d metrics from query %d: %v\n", len(metrics), i, metrics) + result.CheckedQueries++ + queryMetrics[i] = metrics + + // Add to global metrics set + for _, metric := range metrics { + allMetrics[metric] = true + } + } + + // Convert map to slice for fetcher + metricsToCheck := make([]string, 0, len(allMetrics)) + for metric := range allMetrics { + metricsToCheck = append(metricsToCheck, metric) + } + result.TotalMetrics = len(metricsToCheck) + + fmt.Printf("[DEBUG PROM] Total metrics to check: %d - %v\n", len(metricsToCheck), metricsToCheck) + + // Step 2: Fetch available metrics from Prometheus + fmt.Printf("[DEBUG PROM] Fetching available metrics from %s\n", datasource.URL) + availableMetrics, err := v.fetcher.FetchMetrics(ctx, datasource.URL, datasource.HTTPClient) + if err != nil { + fmt.Printf("[DEBUG PROM] Failed to fetch metrics: %v\n", err) + return nil, fmt.Errorf("failed to fetch metrics from Prometheus: %w", err) + } + fmt.Printf("[DEBUG PROM] Fetched %d available metrics from Prometheus\n", len(availableMetrics)) + + // Build a set for O(1) lookup + availableSet := make(map[string]bool) + for _, metric := range availableMetrics { + availableSet[metric] = true + } + + // Step 3: Calculate compatibility + missingMetricsMap := make(map[string]bool) + for _, metric := range metricsToCheck { + if !availableSet[metric] { + missingMetricsMap[metric] = true + } + } + result.FoundMetrics = result.TotalMetrics - len(missingMetricsMap) + + // Convert missing metrics map to slice + result.MissingMetrics = make([]string, 0, len(missingMetricsMap)) + for metric := range missingMetricsMap { + result.MissingMetrics = append(result.MissingMetrics, metric) + } + + // Step 4: Build per-query breakdown + for i, query := range queries { + metrics, ok := queryMetrics[i] + if !ok { + // Query wasn't parsed successfully, skip + continue + } + + queryResult := validator.QueryResult{ + PanelTitle: query.PanelTitle, + PanelID: query.PanelID, + QueryRefID: query.RefID, + TotalMetrics: len(metrics), + } + + // Check which metrics from this query are missing + queryMissing := make([]string, 0) + for _, metric := range metrics { + if missingMetricsMap[metric] { + queryMissing = append(queryMissing, metric) + } + } + + queryResult.MissingMetrics = queryMissing + queryResult.FoundMetrics = queryResult.TotalMetrics - len(queryMissing) + + // Calculate query-level compatibility score + if queryResult.TotalMetrics > 0 { + queryResult.CompatibilityScore = float64(queryResult.FoundMetrics) / float64(queryResult.TotalMetrics) + } else { + queryResult.CompatibilityScore = 1.0 // No metrics = perfect compatibility + } + + result.QueryBreakdown = append(result.QueryBreakdown, queryResult) + } + + // Step 5: Calculate overall compatibility score + if result.TotalMetrics > 0 { + result.CompatibilityScore = float64(result.FoundMetrics) / float64(result.TotalMetrics) + } else { + result.CompatibilityScore = 1.0 // No metrics = perfect compatibility + } + + fmt.Printf("[DEBUG PROM] Validation complete! Score: %.2f, Found: %d/%d metrics\n", + result.CompatibilityScore, result.FoundMetrics, result.TotalMetrics) + + return result, nil +} diff --git a/apps/dashvalidator/pkg/validator/types.go b/apps/dashvalidator/pkg/validator/types.go new file mode 100644 index 00000000000..268524bdb3c --- /dev/null +++ b/apps/dashvalidator/pkg/validator/types.go @@ -0,0 +1,74 @@ +package validator + +import ( + "context" + "fmt" + "net/http" +) + +// DatasourceValidator validates dashboard queries against a datasource +// Implementations exist per datasource type (Prometheus, MySQL, etc.) +type DatasourceValidator interface { + // ValidateQueries checks if queries are compatible with the datasource + ValidateQueries(ctx context.Context, queries []Query, datasource Datasource) (*ValidationResult, error) +} + +// Query represents a dashboard query to validate +type Query struct { + RefID string // Query reference ID (A, B, C, etc.) + QueryText string // The actual query text (PromQL, SQL, etc.) + PanelTitle string // Panel title for user-friendly reporting + PanelID int // Panel ID for reference +} + +// Datasource contains connection information for a datasource +type Datasource struct { + UID string // Datasource UID from dashboard + Type string // Datasource type (prometheus, mysql, etc.) + Name string // Datasource name for reporting + URL string // Datasource URL for API calls + HTTPClient *http.Client // Authenticated HTTP client for making requests +} + +// ValidationResult contains validation results for a datasource +type ValidationResult struct { + TotalQueries int // Total number of queries found + CheckedQueries int // Number of queries successfully checked + TotalMetrics int // Total metrics/entities referenced + FoundMetrics int // Metrics found in datasource + MissingMetrics []string // List of missing metrics + QueryBreakdown []QueryResult // Per-query results + CompatibilityScore float64 // Overall compatibility (0.0 - 1.0) +} + +// QueryResult contains validation results for a single query +type QueryResult struct { + PanelTitle string // Panel title + PanelID int // Panel ID + QueryRefID string // Query reference ID + TotalMetrics int // Metrics in this query + FoundMetrics int // Metrics found + MissingMetrics []string // Missing metrics for this query + CompatibilityScore float64 // Query compatibility (0.0 - 1.0) +} + +// validatorRegistry holds registered validator constructors +// Validators register themselves using RegisterValidator in their init() functions +var validatorRegistry = make(map[string]func() DatasourceValidator) + +// RegisterValidator registers a validator constructor for a datasource type +// This is called by validator implementations in their init() functions +// Example: validator.RegisterValidator("prometheus", func() validator.DatasourceValidator { return NewValidator() }) +func RegisterValidator(dsType string, constructor func() DatasourceValidator) { + validatorRegistry[dsType] = constructor +} + +// GetValidator returns a validator for the given datasource type +// Returns an error if the datasource type is not supported +func GetValidator(dsType string) (DatasourceValidator, error) { + constructor, ok := validatorRegistry[dsType] + if !ok { + return nil, fmt.Errorf("unsupported datasource type: %s", dsType) + } + return constructor(), nil +} diff --git a/pkg/registry/apps/dashvalidator/register.go b/pkg/registry/apps/dashvalidator/register.go index 5924718b370..5f4d051b441 100644 --- a/pkg/registry/apps/dashvalidator/register.go +++ b/pkg/registry/apps/dashvalidator/register.go @@ -9,6 +9,7 @@ import ( validatorapis "github.com/grafana/grafana/apps/dashvalidator/pkg/apis/manifestdata" validatorapp "github.com/grafana/grafana/apps/dashvalidator/pkg/app" + "github.com/grafana/grafana/pkg/infra/httpclient" roleauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" @@ -24,11 +25,13 @@ type DashValidatorAppInstaller struct { func RegisterAppInstaller( datasourceSvc datasources.DataSourceService, pluginCtx *plugincontext.Provider, + httpClientProvider httpclient.Provider, ) (*DashValidatorAppInstaller, error) { // Create specific config for the app specificConfig := &validatorapp.DashValidatorConfig{ - DatasourceSvc: datasourceSvc, - PluginCtx: pluginCtx, + DatasourceSvc: datasourceSvc, + PluginCtx: pluginCtx, + HTTPClientProvider: httpClientProvider, } // Create the app provider diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 44838906d6e..a90cebeacc3 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -832,7 +832,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider) + dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider, httpclientProvider) if err != nil { return nil, err } @@ -1498,7 +1498,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider) + dashValidatorAppInstaller, err := dashvalidator.RegisterAppInstaller(service15, plugincontextProvider, httpclientProvider) if err != nil { return nil, err }