diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go new file mode 100644 index 00000000000..3760429e84d --- /dev/null +++ b/pkg/services/featuremgmt/codeowners.go @@ -0,0 +1,15 @@ +package featuremgmt + +// codeowner string that references a GH team or user +// the value must match the format used in the CODEOWNERS file +type codeowner string + +const ( + grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" + grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" + grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" + grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" + grafanaUserEssentialsSquad codeowner = "@grafana/user-essentials" + grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" + grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend" +) diff --git a/pkg/services/featuremgmt/features.go b/pkg/services/featuremgmt/features.go index 8a19c794f3b..c74eaacfa32 100644 --- a/pkg/services/featuremgmt/features.go +++ b/pkg/services/featuremgmt/features.go @@ -85,6 +85,9 @@ type FeatureFlag struct { State FeatureFlagState `json:"state,omitempty"` DocsURL string `json:"docsURL,omitempty"` + // Owner person or team that owns this feature flag + Owner codeowner `json:"-"` + // CEL-GO expression. Using the value "true" will mean this is on by default Expression string `json:"expression,omitempty"` diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 57fb12c13f1..198315ee53e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -33,28 +33,33 @@ var ( Name: "dashboardPreviews", Description: "Create and show thumbnails for dashboard search results", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "live-pipeline", Description: "Enable a generic live processing pipeline", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "live-service-web-worker", Description: "This will use a webworker thread to processes events rather than the main thread", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaAppPlatformSquad, }, { Name: "queryOverLive", Description: "Use Grafana Live WebSocket to execute backend queries", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaAppPlatformSquad, }, { Name: "panelTitleSearch", Description: "Search for dashboards using panel title", State: FeatureStateBeta, + Owner: grafanaAppPlatformSquad, }, { Name: "prometheusAzureOverrideAudience", @@ -65,6 +70,7 @@ var ( Name: "publicDashboards", Description: "Enables public access to dashboards", State: FeatureStateAlpha, + Owner: grafanaDashboardsSquad, }, { Name: "publicDashboardsEmailSharing", @@ -72,11 +78,13 @@ var ( State: FeatureStateAlpha, RequiresLicense: true, RequiresDevMode: true, + Owner: grafanaDashboardsSquad, }, { Name: "lokiLive", Description: "Support WebSocket streaming for loki (early prototype)", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "lokiDataframeApi", @@ -92,11 +100,13 @@ var ( Name: "dashboardComments", Description: "Enable dashboard-wide comments", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "annotationComments", Description: "Enable annotation comments", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "migrationLocking", @@ -107,18 +117,21 @@ var ( Name: "storage", Description: "Configurable storage for dashboards, datasources, and resources", State: FeatureStateAlpha, + Owner: grafanaAppPlatformSquad, }, { Name: "k8s", Description: "Explore native k8s integrations", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "dashboardsFromStorage", Description: "Load dashboards from the generic storage interface", State: FeatureStateAlpha, RequiresDevMode: true, // Also a gate on automatic git storage (for now) + Owner: grafanaAppPlatformSquad, }, { Name: "exploreMixedDatasource", @@ -153,6 +166,7 @@ var ( Name: "datasourceQueryMultiStatus", Description: "Introduce HTTP 207 Multi Status for api/ds/query", State: FeatureStateAlpha, + Owner: grafanaPluginsPlatformSquad, }, { Name: "traceToMetrics", @@ -164,6 +178,7 @@ var ( Name: "newDBLibrary", Description: "Use jmoiron/sqlx rather than xorm for a few backend services", State: FeatureStateBeta, + Owner: grafanaBackendPlatformSquad, }, { Name: "validateDashboardsOnSave", @@ -176,6 +191,7 @@ var ( Description: "Replace the angular graph panel with timeseries", State: FeatureStateBeta, FrontendOnly: true, + Owner: grafanaDatavizSquad, }, { Name: "prometheusWideSeries", @@ -187,12 +203,14 @@ var ( Description: "Allow elements nesting", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaDatavizSquad, }, { Name: "scenes", Description: "Experimental framework to build interactive dashboards", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaDashboardsSquad, }, { Name: "disableSecretsCompatibility", @@ -215,23 +233,27 @@ var ( Description: "Enables internationalization", State: FeatureStateStable, Expression: "true", // enabled by default + Owner: grafanaUserEssentialsSquad, }, { Name: "topnav", Description: "Displays new top nav and page layouts", State: FeatureStateBeta, + Owner: grafanaUserEssentialsSquad, }, { Name: "grpcServer", Description: "Run GRPC server", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "entityStore", Description: "SQL-based entity store (requires storage flag also)", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "cloudWatchCrossAccountQuerying", @@ -262,6 +284,7 @@ var ( Description: "Reusable query library", State: FeatureStateAlpha, RequiresDevMode: true, + Owner: grafanaAppPlatformSquad, }, { Name: "showDashboardValidationWarnings", @@ -324,6 +347,7 @@ var ( Description: "Enables drag and drop for CSV and Excel files", FrontendOnly: true, State: FeatureStateAlpha, + Owner: grafanaBiSquad, }, { Name: "alertingNoNormalState", @@ -361,6 +385,7 @@ var ( Description: "Changes the user experience for data source selection to a drawer.", State: FeatureStateAlpha, FrontendOnly: true, + Owner: grafanaBiSquad, }, { Name: "traceqlSearch", diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 6e2d29b2ba9..0637408e794 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -41,6 +41,69 @@ func TestFeatureToggleFiles(t *testing.T) { } }) + ownerlessFeatures := map[string]bool{ + "alertingBigTransactions": true, + "trimDefaults": true, + "disableEnvelopeEncryption": true, + "database_metrics": true, + "prometheusAzureOverrideAudience": true, + "lokiDataframeApi": true, + "featureHighlights": true, + "migrationLocking": true, + "exploreMixedDatasource": true, + "tracing": true, + "newTraceView": true, + "correlations": true, + "cloudWatchDynamicLabels": true, + "traceToMetrics": true, + "validateDashboardsOnSave": true, + "prometheusWideSeries": true, + "disableSecretsCompatibility": true, + "logRequestsInstrumentedAsUnknown": true, + "dataConnectionsConsole": true, + "cloudWatchCrossAccountQuerying": true, + "redshiftAsyncQueryDataSupport": true, + "athenaAsyncQueryDataSupport": true, + "newPanelChromeUI": true, + "showDashboardValidationWarnings": true, + "mysqlAnsiQuotes": true, + "accessControlOnCall": true, + "nestedFolders": true, + "accessTokenExpirationCheck": true, + "elasticsearchBackendMigration": true, + "datasourceOnboarding": true, + "secureSocksDatasourceProxy": true, + "authnService": true, + "disablePrometheusExemplarSampling": true, + "alertingBacktesting": true, + "alertingNoNormalState": true, + "logsSampleInExplore": true, + "logsContextDatasourceUi": true, + "lokiQuerySplitting": true, + "individualCookiePreferences": true, + "traceqlSearch": true, + } + + t.Run("all new features should have an owner", func(t *testing.T) { + for _, flag := range standardFeatureFlags { + if flag.Owner == "" { + if _, ok := ownerlessFeatures[flag.Name]; !ok { + t.Errorf("feature %s does not have an owner", flag.Name) + } + } + } + }) + + t.Run("features with assigned owner should not be on the ownerless list", func(t *testing.T) { + for _, flag := range standardFeatureFlags { + if flag.Owner != "" { + if _, ok := ownerlessFeatures[flag.Name]; ok { + t.Errorf("feature %s should be removed from the ownerless list", flag.Name) + } + } + } + }) + t.Run("verify files", func(t *testing.T) { // Typescript files verifyAndGenerateFile(t,