diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 74f26f97f6f..be7a2e0d582 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -50,6 +50,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | | `formatString` | Enable format string transformer | Yes | | `transformationsVariableSupport` | Allows using variables in transformations | Yes | +| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | Yes | | `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes | | `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes | | `managedPluginsInstall` | Install managed plugins directly from plugins catalog | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e009b3da487..1c577afa5f0 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -113,6 +113,7 @@ export interface FeatureToggles { disableClassicHTTPHistogram?: boolean; formatString?: boolean; transformationsVariableSupport?: boolean; + kubernetesPlaylists?: boolean; kubernetesSnapshots?: boolean; kubernetesDashboards?: boolean; datasourceQueryTypes?: boolean; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 5b0d48f5e1c..b2bc145eacf 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -185,7 +185,6 @@ export class GrafanaBootConfig implements GrafanaConfig { cloudMigrationPollIntervalMs = 2000; reportingStaticContext?: Record; exploreDefaultTimeOffset = '1h'; - unifiedStorage: Map = new Map(); /** * Language used in Grafana's UI. This is after the user's preference (or deteceted locale) is resolved to one of diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index d6d3b844a28..b395fad006e 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -12,12 +12,13 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - playlistalpha1 "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" "github.com/grafana/grafana/pkg/middleware" internalplaylist "github.com/grafana/grafana/pkg/registry/apis/playlist" grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/util/errhttp" "github.com/grafana/grafana/pkg/web" @@ -26,8 +27,7 @@ import ( func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) { // Register the actual handlers apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) { - unifiedStorageOptions := hs.Cfg.UnifiedStorage - if mode, ok := unifiedStorageOptions[playlistalpha1.GROUPRESOURCE]; ok && mode > 0 { + if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesPlaylists) { // Use k8s client to implement legacy API handler := newPlaylistK8sHandler(hs) playlistRoute.Get("/", handler.searchPlaylists) @@ -330,7 +330,7 @@ type playlistK8sHandler struct { func newPlaylistK8sHandler(hs *HTTPServer) *playlistK8sHandler { return &playlistK8sHandler{ - gvr: playlistalpha1.PlaylistResourceInfo.GroupVersionResource(), + gvr: v0alpha1.PlaylistResourceInfo.GroupVersionResource(), namespacer: request.GetNamespaceMapper(hs.Cfg), clientConfigProvider: hs.clientConfigProvider, } diff --git a/pkg/services/apiserver/README.md b/pkg/services/apiserver/README.md index 80eaafe5ff2..7a65d16e9e3 100644 --- a/pkg/services/apiserver/README.md +++ b/pkg/services/apiserver/README.md @@ -4,6 +4,7 @@ ```ini [feature_toggles] +kubernetesPlaylists = true ``` Start Grafana: @@ -61,10 +62,8 @@ For kubectl to work, grafana needs to run over https. To simplify development, app_mode = development [feature_toggles] -grafanaAPIServerEnsureKubectlAccess = true - -[unified_storage] -playlists.playlist.grafana.app = 2 +grafanaAPIServerEnsureKubectlAccess = true +kubernetesPlaylists = true ``` This will create a development kubeconfig and start a parallel ssl listener. It can be registered by @@ -91,4 +90,4 @@ The folder structure aims to follow the patterns established in standard (https: * [pkg/apis](/pkg/apis) - where API resource types are defined. this is based on the structure of the [sample-apiserver](https://github.com/kubernetes/sample-apiserver/tree/master/pkg/apis) * [hack/update-codegen.sh](/hack#kubernetes-hack-alert) - this script is used to run [k8s codegen](https://github.com/kubernetes/code-generator/), which generates the code that is used by the API server to handle the types defined in `pkg/apis`. it is based on the [update-codegen.sh from sample-apiserver](https://github.com/kubernetes/sample-apiserver/blob/master/hack/update-codegen.sh) * [pkg/registry/apis](/pkg/registry/apis) - where all of the types in `pkg/apis` are registered with the API server by implementing the [builder](/pkg/services/apiserver/builder/common.go#L18) interface. this pattern is unique to grafana, and is needed to support using wire dependencies in legacy storage implementations. this is separated from `pkg/apis` to avoid issues with k8s codegen. -* [pkg/cmd/grafana/apiserver](/pkg/cmd/grafana/apiserver) - this is where the apiserver is configured for the `grafana apiserver` CLI command, which can be used to launch standalone API servers. this will eventually be merged with the config in `pkg/services/apiserver` to reduce duplication. +* [pkg/cmd/grafana/apiserver](/pkg/cmd/grafana/apiserver) - this is where the apiserver is configured for the `grafana apiserver` CLI command, which can be used to launch standalone API servers. this will eventually be merged with the config in `pkg/services/apiserver` to reduce duplication. \ No newline at end of file diff --git a/pkg/services/apiserver/config.go b/pkg/services/apiserver/config.go index 5141702d041..dee64ca4848 100644 --- a/pkg/services/apiserver/config.go +++ b/pkg/services/apiserver/config.go @@ -7,6 +7,7 @@ import ( "strconv" playlist "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/options" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" @@ -54,16 +55,14 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o o.StorageOptions.StorageType = options.StorageType(apiserverCfg.Key("storage_type").MustString(string(options.StorageTypeLegacy))) o.StorageOptions.DataPath = apiserverCfg.Key("storage_path").MustString(filepath.Join(cfg.DataPath, "grafana-apiserver")) o.StorageOptions.Address = apiserverCfg.Key("address").MustString(o.StorageOptions.Address) - - // unified storage modes - unifiedStorageCfg := cfg.UnifiedStorage - o.StorageOptions.DualWriterDesiredModes = unifiedStorageCfg + o.StorageOptions.DualWriterDesiredModes = map[string]grafanarest.DualWriterMode{ + // TODO: use the new config from HGAPI after https://github.com/grafana/hosted-grafana/pull/5707 + playlist.GROUPRESOURCE: 2, + } // TODO: ensure backwards compatibility with production - // remove this after changing the unified_storage key format in HGAPI - if _, ok := o.StorageOptions.DualWriterDesiredModes[playlist.RESOURCE+"."+playlist.GROUP]; ok { - o.StorageOptions.DualWriterDesiredModes[playlist.RESOURCE+"."+playlist.GROUP] = o.StorageOptions.DualWriterDesiredModes[playlist.GROUPRESOURCE] - } + // remove this after changing the unified_storage_mode key format in HGAPI + o.StorageOptions.DualWriterDesiredModes[playlist.RESOURCE+"."+playlist.GROUP] = o.StorageOptions.DualWriterDesiredModes[playlist.GROUPRESOURCE] o.ExtraOptions.DevMode = features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerEnsureKubectlAccess) o.ExtraOptions.ExternalAddress = host diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 0426f7fcf4a..e0f9dcc0d75 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -721,6 +721,14 @@ var ( Owner: grafanaDatavizSquad, Expression: "true", // Enabled by default }, + { + Name: "kubernetesPlaylists", + Description: "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaAppPlatformSquad, + Expression: "true", + RequiresRestart: true, // changes the API routing + }, { Name: "kubernetesSnapshots", Description: "Routes snapshot requests from /api to the /apis endpoint", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 344650d1c7b..63d448a2b73 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -94,6 +94,7 @@ enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,f disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false formatString,GA,@grafana/dataviz-squad,false,false,true transformationsVariableSupport,GA,@grafana/dataviz-squad,false,false,true +kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 90e3724901d..f18393e4f93 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -387,6 +387,10 @@ const ( // Allows using variables in transformations FlagTransformationsVariableSupport = "transformationsVariableSupport" + // FlagKubernetesPlaylists + // Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s + FlagKubernetesPlaylists = "kubernetesPlaylists" + // FlagKubernetesSnapshots // Routes snapshot requests from /api to the /apis endpoint FlagKubernetesSnapshots = "kubernetesSnapshots" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 16585bbf70b..556ed216cbf 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1438,7 +1438,6 @@ "name": "kubernetesPlaylists", "resourceVersion": "1720021873452", "creationTimestamp": "2023-10-05T19:00:36Z", - "deletionTimestamp": "2024-07-30T19:34:12Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 30f4fdca73d..639fab90aee 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -29,7 +29,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/apimachinery/identity" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/osutil" @@ -522,9 +521,6 @@ type Cfg struct { //Short Links ShortLinkExpiration int - - // Unified Storage - UnifiedStorage map[string]grafanarest.DualWriterMode } type InstallPlugin struct { diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index cd7f02037a5..d42b63d5149 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -77,11 +77,24 @@ func TestIntegrationPlaylist(t *testing.T) { ]`, disco) }) + t.Run("with k8s api flag", func(t *testing.T) { + doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, // do not start extra port 6443 + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // <<< The change we are testing! + }, + })) + }) + t.Run("with dual write (file, mode 0)", func(t *testing.T) { doPlaylistTests(t, apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "file", // write the files to disk + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode0, }, @@ -93,6 +106,9 @@ func TestIntegrationPlaylist(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "file", // write the files to disk + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode1, }, @@ -104,6 +120,9 @@ func TestIntegrationPlaylist(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "file", // write the files to disk + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode2, }, @@ -115,6 +134,9 @@ func TestIntegrationPlaylist(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "file", // write the files to disk + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode3, }, @@ -128,6 +150,7 @@ func TestIntegrationPlaylist(t *testing.T) { APIServerStorageType: "unified", // use the entity api tables EnableFeatureToggles: []string{ featuremgmt.FlagUnifiedStorage, + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode0, @@ -142,6 +165,7 @@ func TestIntegrationPlaylist(t *testing.T) { APIServerStorageType: "unified", // use the entity api tables EnableFeatureToggles: []string{ featuremgmt.FlagUnifiedStorage, + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode1, @@ -156,6 +180,7 @@ func TestIntegrationPlaylist(t *testing.T) { APIServerStorageType: "unified", // use the entity api tables EnableFeatureToggles: []string{ featuremgmt.FlagUnifiedStorage, + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode2, @@ -170,6 +195,7 @@ func TestIntegrationPlaylist(t *testing.T) { APIServerStorageType: "unified", // use the entity api tables EnableFeatureToggles: []string{ featuremgmt.FlagUnifiedStorage, + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode3, @@ -185,6 +211,9 @@ func TestIntegrationPlaylist(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "etcd", // requires etcd running on localhost:2379 + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode0, }, @@ -209,7 +238,9 @@ func TestIntegrationPlaylist(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "etcd", // requires etcd running on localhost:2379 - EnableFeatureToggles: []string{}, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode1, }, @@ -234,7 +265,9 @@ func TestIntegrationPlaylist(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "etcd", // requires etcd running on localhost:2379 - EnableFeatureToggles: []string{}, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode2, }, @@ -259,7 +292,9 @@ func TestIntegrationPlaylist(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, APIServerStorageType: "etcd", // requires etcd running on localhost:2379 - EnableFeatureToggles: []string{}, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesPlaylists, // Required so that legacy calls are also written + }, DualWriterDesiredModes: map[string]grafanarest.DualWriterMode{ playlistv0alpha1.GROUPRESOURCE: grafanarest.Mode3, }, diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index f22055f2d64..3da3265607a 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -387,7 +387,7 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) { } if o.DualWriterDesiredModes != nil { - unifiedStorageMode, err := getOrCreateSection("unified_storage") + unifiedStorageMode, err := getOrCreateSection("unified_storage_mode") require.NoError(t, err) for k, v := range o.DualWriterDesiredModes { _, err = unifiedStorageMode.NewKey(k, fmt.Sprint(v)) diff --git a/public/app/features/playlist/api.ts b/public/app/features/playlist/api.ts index 4cc9559dee5..dd778f8903d 100644 --- a/public/app/features/playlist/api.ts +++ b/public/app/features/playlist/api.ts @@ -209,8 +209,5 @@ export function searchPlaylists(playlists: Playlist[], query?: string): Playlist } export function getPlaylistAPI() { - if (config.unifiedStorage) { - return config.unifiedStorage.has('playlist.grafana.app/playlists') ? new K8sAPI() : new LegacyAPI(); - } - return new LegacyAPI(); + return config.featureToggles.kubernetesPlaylists ? new K8sAPI() : new LegacyAPI(); }