Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1b63e4d29 |
@@ -1,6 +1,7 @@
|
||||
package conversion
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -82,6 +83,17 @@ type dashboardStats struct {
|
||||
annotationCount int
|
||||
linkCount int
|
||||
variableCount int
|
||||
jsonSize int
|
||||
}
|
||||
|
||||
// getJSONSize returns the size of an object when serialized to JSON
|
||||
// This is a generic way to detect content loss without knowing the specific structure
|
||||
func getJSONSize(v interface{}) int {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return len(data)
|
||||
}
|
||||
|
||||
// countPanelsV0V1 counts panels in v0alpha1 or v1beta1 dashboard spec (unstructured JSON)
|
||||
@@ -238,6 +250,7 @@ func collectStatsV0V1(spec map[string]interface{}) dashboardStats {
|
||||
annotationCount: countAnnotationsV0V1(spec),
|
||||
linkCount: countLinksV0V1(spec),
|
||||
variableCount: countVariablesV0V1(spec),
|
||||
jsonSize: getJSONSize(spec),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +302,7 @@ func collectStatsV2alpha1(spec dashv2alpha1.DashboardSpec) dashboardStats {
|
||||
annotationCount: countAnnotationsV2(spec.Annotations),
|
||||
linkCount: countLinksV2(spec.Links),
|
||||
variableCount: countVariablesV2(spec.Variables),
|
||||
jsonSize: getJSONSize(spec),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +354,7 @@ func collectStatsV2beta1(spec dashv2beta1.DashboardSpec) dashboardStats {
|
||||
annotationCount: countAnnotationsV2beta1(spec.Annotations),
|
||||
linkCount: countLinksV2beta1(spec.Links),
|
||||
variableCount: countVariablesV2beta1(spec.Variables),
|
||||
jsonSize: getJSONSize(spec),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +415,20 @@ func detectConversionDataLoss(sourceStats, targetStats dashboardStats, sourceFun
|
||||
))
|
||||
}
|
||||
|
||||
// JSON size: detect significant decrease (>20%) which may indicate content loss
|
||||
// This catches cases like text panel content being replaced with defaults
|
||||
if sourceStats.jsonSize > 0 && targetStats.jsonSize > 0 {
|
||||
decreasePercent := float64(sourceStats.jsonSize-targetStats.jsonSize) / float64(sourceStats.jsonSize) * 100
|
||||
if decreasePercent > 20 {
|
||||
errors = append(errors, fmt.Sprintf(
|
||||
"JSON size decreased significantly: source=%d bytes, target=%d bytes (%.1f%% decrease, possible content loss)",
|
||||
sourceStats.jsonSize,
|
||||
targetStats.jsonSize,
|
||||
decreasePercent,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
errorMsg := fmt.Sprintf("%v", errors)
|
||||
// Note: sourceAPIVersion and targetAPIVersion are passed from checkConversionDataLoss
|
||||
|
||||
@@ -189,6 +189,8 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion
|
||||
"annotationsLost", math.Max(0, float64(sourceStats.annotationCount-targetStats.annotationCount)),
|
||||
"linksLost", math.Max(0, float64(sourceStats.linkCount-targetStats.linkCount)),
|
||||
"variablesLost", math.Max(0, float64(sourceStats.variableCount-targetStats.variableCount)),
|
||||
"sourceJSONSize", sourceStats.jsonSize,
|
||||
"targetJSONSize", targetStats.jsonSize,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -222,11 +224,19 @@ func withConversionMetrics(sourceVersionAPI, targetVersionAPI string, conversion
|
||||
).Inc()
|
||||
|
||||
// Log success (debug level to avoid spam)
|
||||
// Collect stats for logging
|
||||
sourceStats := collectDashboardStats(a)
|
||||
targetStats := collectDashboardStats(b)
|
||||
|
||||
// Build base log fields for success
|
||||
successLogFields := []interface{}{
|
||||
"sourceVersionAPI", sourceVersionAPI,
|
||||
"targetVersionAPI", targetVersionAPI,
|
||||
"dashboardUID", dashboardUID,
|
||||
"panelCount", targetStats.panelCount,
|
||||
"queryCount", targetStats.queryCount,
|
||||
"sourceJsonSize", sourceStats.jsonSize,
|
||||
"targetJsonSize", targetStats.jsonSize,
|
||||
}
|
||||
|
||||
// Add schema version fields only if we have them (v0/v1 dashboards)
|
||||
|
||||
@@ -699,6 +699,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
playlistsReconciler?: boolean;
|
||||
/**
|
||||
* Enable passwordless login via magic link authentication
|
||||
*/
|
||||
passwordlessMagicLinkAuthentication?: boolean;
|
||||
/**
|
||||
* Display Related Logs in Grafana Metrics Drilldown
|
||||
*/
|
||||
exploreMetricsRelatedLogs?: boolean;
|
||||
|
||||
+2
-1
@@ -226,7 +226,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Post("/api/user/email/start-verify", reqSignedInNoAnonymous, routing.Wrap(hs.StartEmailVerificaton))
|
||||
}
|
||||
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabledGlobally(featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
r.Post("/api/login/passwordless/start", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), hs.StartPasswordless)
|
||||
r.Post("/api/login/passwordless/authenticate", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPasswordless))
|
||||
}
|
||||
|
||||
@@ -410,7 +410,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
DisableSignoutMenu: hs.Cfg.DisableSignoutMenu,
|
||||
}
|
||||
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
hasEnabledProviders := hs.samlEnabled() || hs.authnService.IsClientEnabled(authn.ClientLDAP)
|
||||
|
||||
if !hasEnabledProviders {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package authnimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -81,7 +83,8 @@ func ProvideRegistration(
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if cfg.PasswordlessMagicLinkAuth.Enabled && features.IsEnabled(context.Background(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
hasEnabledProviders := authnSvc.IsClientEnabled(authn.ClientSAML) || authnSvc.IsClientEnabled(authn.ClientLDAP)
|
||||
if !hasEnabledProviders {
|
||||
oauthInfos := socialService.GetOAuthInfoProviders()
|
||||
|
||||
@@ -1155,7 +1155,14 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
{
|
||||
Name: "passwordlessMagicLinkAuthentication",
|
||||
Description: "Enable passwordless login via magic link authentication",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: identityAccessTeam,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "exploreMetricsRelatedLogs",
|
||||
Description: "Display Related Logs in Grafana Metrics Drilldown",
|
||||
Stage: FeatureStageExperimental,
|
||||
|
||||
Generated
+1
@@ -160,6 +160,7 @@ timeRangePan,experimental,@grafana/dataviz-squad,false,false,true
|
||||
newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true
|
||||
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
|
||||
playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false
|
||||
exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true
|
||||
prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true
|
||||
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
|
||||
|
||||
|
Generated
+4
@@ -483,6 +483,10 @@ const (
|
||||
// Enables experimental reconciler for playlists
|
||||
FlagPlaylistsReconciler = "playlistsReconciler"
|
||||
|
||||
// FlagPasswordlessMagicLinkAuthentication
|
||||
// Enable passwordless login via magic link authentication
|
||||
FlagPasswordlessMagicLinkAuthentication = "passwordlessMagicLinkAuthentication"
|
||||
|
||||
// FlagEnableExtensionsAdminPage
|
||||
// Enables the extension admin page regardless of development mode
|
||||
FlagEnableExtensionsAdminPage = "enableExtensionsAdminPage"
|
||||
|
||||
+1
-2
@@ -2661,8 +2661,7 @@
|
||||
"metadata": {
|
||||
"name": "passwordlessMagicLinkAuthentication",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-11-14T13:50:55Z",
|
||||
"deletionTimestamp": "2026-01-08T15:33:29Z"
|
||||
"creationTimestamp": "2024-11-14T13:50:55Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable passwordless login via magic link authentication",
|
||||
|
||||
Reference in New Issue
Block a user