Improve error handling for validator

- Surface error codes for datasource possible errors (not found, unreachabel, auth, timeout)
This commit is contained in:
alexandra vargas
2026-01-06 13:27:34 +01:00
parent f81deced02
commit 01f959be97
5 changed files with 600 additions and 23 deletions
+93 -8
View File
@@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"strings"
)
type DashValidatorConfig struct {
@@ -143,35 +144,80 @@ func handleCheckRoute(
logger.Info("Processing request", "dashboardTitle", req.DashboardJSON["title"], "numMappings", len(req.DatasourceMappings))
// Get namespace from request (needed for datasource lookup)
// Namespace format is typically "org-{orgID}"
namespace := r.ResourceIdentifier.Namespace
// Extract orgID from namespace for logging context
orgID := extractOrgIDFromNamespace(namespace)
logger = logger.With("orgID", orgID, "namespace", namespace)
for _, dsMapping := range req.DatasourceMappings {
dsLogger := logger.With("datasourceUID", dsMapping.UID, "datasourceType", dsMapping.Type)
// Convert optional name pointer to string
name := ""
if dsMapping.Name != nil {
name = *dsMapping.Name
dsLogger = dsLogger.With("datasourceName", name)
}
// Fetch datasource from Grafana using app-platform method
// Parameters: namespace, name (UID), group (datasource type)
ds, err := datasourceSvc.GetDataSourceInNamespace(ctx, namespace, dsMapping.UID, dsMapping.Type)
if err != nil {
logger.Error("Failed to get datasource", "namespace", namespace, "uid", dsMapping.UID, "type", dsMapping.Type, "error", err)
w.WriteHeader(http.StatusBadRequest)
dsLogger.Error("Failed to get datasource from namespace", "error", err)
// Check if it's a not found error vs other errors
errMsg := err.Error()
statusCode := http.StatusInternalServerError
userMsg := fmt.Sprintf("failed to retrieve datasource: %s", dsMapping.UID)
if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "does not exist") {
statusCode = http.StatusNotFound
userMsg = fmt.Sprintf("datasource not found: %s (type: %s)", dsMapping.UID, dsMapping.Type)
dsLogger.Warn("Datasource not found in namespace")
}
w.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource not found: %s", dsMapping.UID),
"error": userMsg,
"code": "datasource_error",
})
}
logger.Info("Retrieved datasource", "uid", ds.UID, "url", ds.URL, "type", ds.Type)
dsLogger.Info("Retrieved datasource", "url", ds.URL, "actualType", ds.Type)
// Validate that the datasource type matches the expected type
if ds.Type != dsMapping.Type {
dsLogger.Error("Datasource type mismatch",
"expectedType", dsMapping.Type,
"actualType", ds.Type)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource %s has type %s, expected %s", dsMapping.UID, ds.Type, dsMapping.Type),
"code": "datasource_wrong_type",
})
}
// Validate that this is a supported datasource type
// For MVP, we only support Prometheus
if !isSupportedDatasourceType(ds.Type) {
dsLogger.Error("Unsupported datasource type", "type", ds.Type)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource type '%s' is not supported (currently only 'prometheus' is supported)", ds.Type),
"code": "datasource_unsupported_type",
})
}
// Get authenticated HTTP transport for this datasource
transport, err := datasourceSvc.GetHTTPTransport(ctx, ds, httpClientProvider)
if err != nil {
logger.Error("Failed to get HTTP transport", "uid", ds.UID, "error", err)
dsLogger.Error("Failed to get HTTP transport for datasource", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("failed to configure datasource transport: %s", dsMapping.UID),
"error": fmt.Sprintf("failed to configure authentication for datasource: %s", dsMapping.UID),
"code": "datasource_config_error",
})
}
@@ -187,15 +233,35 @@ func handleCheckRoute(
URL: ds.URL,
HTTPClient: httpClient, // Pass authenticated client
})
dsLogger.Debug("Datasource configured successfully for validation")
}
// Step 3: Validate dashboard compatibility
result, err := validator.ValidateDashboardCompatibility(ctx, validatorReq)
if err != nil {
logger.Error("Validation failed", "error", err)
w.WriteHeader(http.StatusInternalServerError)
// Check if it's a structured ValidationError with a specific status code
statusCode := http.StatusInternalServerError
errorCode := "validation_error"
errorMsg := fmt.Sprintf("validation failed: %v", err)
if validationErr := validator.GetValidationError(err); validationErr != nil {
statusCode = validationErr.StatusCode
errorCode = string(validationErr.Code)
errorMsg = validationErr.Message
// Log additional context from the error
for key, value := range validationErr.Details {
logger.Error("Validation error detail", key, value)
}
}
w.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("validation failed: %v", err),
"error": errorMsg,
"code": errorCode,
})
}
@@ -253,6 +319,25 @@ func convertToCheckResponse(result *validator.DashboardCompatibilityResult) chec
return response
}
// extractOrgIDFromNamespace extracts the org ID from a namespace string
// Namespace format is typically "org-{orgID}"
func extractOrgIDFromNamespace(namespace string) string {
parts := strings.Split(namespace, "-")
if len(parts) >= 2 && parts[0] == "org" {
return parts[1]
}
return "unknown"
}
// isSupportedDatasourceType checks if a datasource type is supported
// For MVP, we only support Prometheus
func isSupportedDatasourceType(dsType string) bool {
supportedTypes := map[string]bool{
"prometheus": true,
}
return supportedTypes[strings.ToLower(dsType)]
}
func GetKinds() map[schema.GroupVersion][]resource.Kind {
gv := schema.GroupVersion{
Group: "dashvalidator.grafana.com",