mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
K8s/FeatureFlags: Add an apiserver to manage feature flags (dev only) (#80501)
* add deployment registry API cloud only * update versions * add feature flag endpoints * use helpers * merge main * update AllowSelfServie and re-run code gen * fix package name * add allowselfserve flag to payload * remove config * update list api to return the full registry including states * change enabled check * fix compile error * add feature toggle and split path in frontend * changes * with status * add more status/state * add back config thing * add back config thing * merge main * merge main * now on the /current api endpoint * now on the /current api endpoint * drop frontend changes * change group name to featuretoggle (singular) * use the same settings * now with patch * more common refs * more common refs * WIP actually do the webhook * fix comment * fewer imports * registe standalone * one less file * fix singular name --------- Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
This commit is contained in:
@@ -170,6 +170,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `flameGraphItemCollapsing` | Allow collapsing of flame graph items |
|
||||
| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes |
|
||||
| `tableSharedCrosshair` | Enables shared crosshair in table panel |
|
||||
| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend |
|
||||
| `enablePluginsTracingByDefault` | Enable plugin tracing for all external plugins |
|
||||
| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled |
|
||||
|
||||
|
@@ -14,6 +14,8 @@ total 0
|
||||
lrwxr-xr-x 1 ryan staff 37 Oct 5 09:34 grafana -> /Users/ryan/workspace/grafana/grafana
|
||||
```
|
||||
|
||||
You can clone k8s [code-generator](https://github.com/kubernetes/code-generator) here and use `CODEGEN_PKG=<CODE-GENERATOR-GIT-ROOT>` when running the `update-codegen.sh` script.
|
||||
|
||||
The current workflow (sorry!) is to:
|
||||
|
||||
1. update the script to point to the group+version you want
|
||||
|
@@ -169,6 +169,7 @@ export interface FeatureToggles {
|
||||
displayAnonymousStats?: boolean;
|
||||
alertStateHistoryAnnotationsFromLoki?: boolean;
|
||||
lokiQueryHints?: boolean;
|
||||
kubernetesFeatureToggles?: boolean;
|
||||
alertingPreviewUpgrade?: boolean;
|
||||
enablePluginsTracingByDefault?: boolean;
|
||||
cloudRBACRoles?: boolean;
|
||||
|
@@ -18,22 +18,20 @@ import (
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.Response {
|
||||
cfg := hs.Cfg.FeatureManagement
|
||||
enabledFeatures := hs.Features.GetEnabled(ctx.Req.Context())
|
||||
|
||||
// object being returned
|
||||
dtos := make([]featuremgmt.FeatureToggleDTO, 0)
|
||||
|
||||
// loop through features an add features that should be visible to dtos
|
||||
for _, ft := range hs.featureManager.GetFlags() {
|
||||
if isFeatureHidden(ft, cfg.HiddenToggles) {
|
||||
flag := ft.Name
|
||||
if hs.featureManager.IsHiddenFromAdminPage(flag, false) {
|
||||
continue
|
||||
}
|
||||
dto := featuremgmt.FeatureToggleDTO{
|
||||
Name: ft.Name,
|
||||
Name: flag,
|
||||
Description: ft.Description,
|
||||
Enabled: enabledFeatures[ft.Name],
|
||||
ReadOnly: !isFeatureWriteable(ft, cfg.ReadOnlyToggles) || !isFeatureEditingAllowed(*hs.Cfg),
|
||||
Enabled: hs.featureManager.IsEnabled(ctx.Req.Context(), flag),
|
||||
ReadOnly: !hs.featureManager.IsEditableFromAdminPage(flag),
|
||||
}
|
||||
|
||||
dtos = append(dtos, dto)
|
||||
@@ -46,7 +44,7 @@ func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.R
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response.Response {
|
||||
featureMgmtCfg := hs.Cfg.FeatureManagement
|
||||
featureMgmtCfg := hs.featureManager.Settings
|
||||
if !featureMgmtCfg.AllowEditing {
|
||||
return response.Error(http.StatusForbidden, "feature toggles are read-only", fmt.Errorf("feature toggles are configured to be read-only"))
|
||||
}
|
||||
@@ -67,7 +65,7 @@ func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response
|
||||
|
||||
for _, t := range cmd.FeatureToggles {
|
||||
// make sure flag exists, and only continue if flag is writeable
|
||||
if f, ok := hs.featureManager.LookupFlag(t.Name); ok && isFeatureWriteable(f, hs.Cfg.FeatureManagement.ReadOnlyToggles) {
|
||||
if hs.featureManager.IsEditableFromAdminPage(t.Name) {
|
||||
hs.log.Info("UpdateFeatureToggle: updating toggle", "toggle_name", t.Name, "enabled", t.Enabled, "username", ctx.SignedInUser.Login)
|
||||
payload.FeatureToggles[t.Name] = strconv.FormatBool(t.Enabled)
|
||||
} else {
|
||||
@@ -92,32 +90,6 @@ func (hs *HTTPServer) GetFeatureMgmtState(ctx *contextmodel.ReqContext) response
|
||||
return response.Respond(http.StatusOK, fmState)
|
||||
}
|
||||
|
||||
// isFeatureHidden returns whether a toggle should be hidden from the admin page.
|
||||
// filters out statuses Unknown, Experimental, and Private Preview
|
||||
func isFeatureHidden(flag featuremgmt.FeatureFlag, hideCfg map[string]struct{}) bool {
|
||||
if _, ok := hideCfg[flag.Name]; ok {
|
||||
return true
|
||||
}
|
||||
return flag.Stage == featuremgmt.FeatureStageUnknown || flag.Stage == featuremgmt.FeatureStageExperimental || flag.Stage == featuremgmt.FeatureStagePrivatePreview || flag.HideFromAdminPage
|
||||
}
|
||||
|
||||
// isFeatureWriteable returns whether a toggle on the admin page can be updated by the user.
|
||||
// only allows writing of GA and Deprecated toggles, and excludes the feature toggle admin page toggle
|
||||
func isFeatureWriteable(flag featuremgmt.FeatureFlag, readOnlyCfg map[string]struct{}) bool {
|
||||
if _, ok := readOnlyCfg[flag.Name]; ok {
|
||||
return false
|
||||
}
|
||||
if flag.Name == featuremgmt.FlagFeatureToggleAdminPage {
|
||||
return false
|
||||
}
|
||||
return (flag.Stage == featuremgmt.FeatureStageGeneralAvailability || flag.Stage == featuremgmt.FeatureStageDeprecated) && flag.AllowSelfServe
|
||||
}
|
||||
|
||||
// isFeatureEditingAllowed checks if the backend is properly configured to allow feature toggle changes from the UI
|
||||
func isFeatureEditingAllowed(cfg setting.Cfg) bool {
|
||||
return cfg.FeatureManagement.AllowEditing && cfg.FeatureManagement.UpdateWebhook != ""
|
||||
}
|
||||
|
||||
type UpdatePayload struct {
|
||||
FeatureToggles map[string]string `json:"feature_toggles"`
|
||||
User string `json:"user"`
|
||||
|
@@ -393,9 +393,8 @@ func runGetScenario(
|
||||
) []featuremgmt.FeatureToggleDTO {
|
||||
// Set up server and send request
|
||||
cfg := setting.NewCfg()
|
||||
cfg.FeatureManagement = settings
|
||||
|
||||
fm := featuremgmt.WithFeatureManager(append([]*featuremgmt.FeatureFlag{{
|
||||
fm := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{
|
||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}}, features...), disabled...)
|
||||
@@ -460,9 +459,7 @@ func runSetScenario(
|
||||
) *http.Response {
|
||||
// Set up server and send request
|
||||
cfg := setting.NewCfg()
|
||||
cfg.FeatureManagement = settings
|
||||
|
||||
features := featuremgmt.WithFeatureManager(append([]*featuremgmt.FeatureFlag{{
|
||||
features := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{
|
||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}}, serverFeatures...), disabled...)
|
||||
|
17
pkg/apis/common/v0alpha1/types.go
Normal file
17
pkg/apis/common/v0alpha1/types.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package v0alpha1
|
||||
|
||||
// Similar to
|
||||
// https://dev-k8sref-io.web.app/docs/common-definitions/objectreference-/
|
||||
// ObjectReference contains enough information to let you inspect or modify the referred object.
|
||||
type ObjectReference struct {
|
||||
Resource string `json:"resource,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// APIGroup is the name of the API group that contains the referred object.
|
||||
// The empty string represents the core API group.
|
||||
APIGroup string `json:"apiGroup,omitempty"`
|
||||
|
||||
// APIVersion is the version of the API group that contains the referred object.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
}
|
@@ -17,59 +17,105 @@ import (
|
||||
|
||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref),
|
||||
"k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref),
|
||||
"k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref),
|
||||
"k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.ObjectReference": schema_pkg_apis_common_v0alpha1_ObjectReference(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref),
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref),
|
||||
"k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref),
|
||||
"k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref),
|
||||
"k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref),
|
||||
"k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref),
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_common_v0alpha1_ObjectReference(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Similar to https://dev-k8sref-io.web.app/docs/common-definitions/objectreference-/ ObjectReference contains enough information to let you inspect or modify the referred object.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"resource": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"namespace": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiGroup": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIGroup is the name of the API group that contains the referred object. The empty string represents the core API group.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion is the version of the API group that contains the referred object.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
5
pkg/apis/featuretoggle/v0alpha1/doc.go
Normal file
5
pkg/apis/featuretoggle/v0alpha1/doc.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:openapi-gen=true
|
||||
// +groupName=featuretoggle.grafana.app
|
||||
|
||||
package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
33
pkg/apis/featuretoggle/v0alpha1/register.go
Normal file
33
pkg/apis/featuretoggle/v0alpha1/register.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
GROUP = "featuretoggle.grafana.app"
|
||||
VERSION = "v0alpha1"
|
||||
APIVERSION = GROUP + "/" + VERSION
|
||||
)
|
||||
|
||||
// FeatureResourceInfo represents each feature that may have a toggle
|
||||
var FeatureResourceInfo = common.NewResourceInfo(GROUP, VERSION,
|
||||
"features", "feature", "Feature",
|
||||
func() runtime.Object { return &Feature{} },
|
||||
func() runtime.Object { return &FeatureList{} },
|
||||
)
|
||||
|
||||
// TogglesResourceInfo represents the actual configuration
|
||||
var TogglesResourceInfo = common.NewResourceInfo(GROUP, VERSION,
|
||||
"featuretoggles", "featuretoggle", "FeatureToggles",
|
||||
func() runtime.Object { return &FeatureToggles{} },
|
||||
func() runtime.Object { return &FeatureTogglesList{} },
|
||||
)
|
||||
|
||||
var (
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}
|
||||
)
|
111
pkg/apis/featuretoggle/v0alpha1/types.go
Normal file
111
pkg/apis/featuretoggle/v0alpha1/types.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
// Feature represents a feature in development and information about that feature
|
||||
// It does *not* know the status, only defines properties about the feature itself
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type Feature struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec FeatureSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
type FeatureSpec struct {
|
||||
// The feature description
|
||||
Description string `json:"description"`
|
||||
|
||||
// Indicates the features level of stability
|
||||
Stage string `json:"stage"`
|
||||
|
||||
// The team who owns this feature development
|
||||
Owner string `json:"codeowner,omitempty"`
|
||||
|
||||
// Enabled by default for version >=
|
||||
EnabledVersion string `json:"enabledVersion,omitempty"`
|
||||
|
||||
// Must be run using in development mode (early dev)
|
||||
RequiresDevMode bool `json:"requiresDevMode,omitempty"`
|
||||
|
||||
// The flab behavior only effects frontend -- it is not used in the backend
|
||||
FrontendOnly bool `json:"frontend,omitempty"`
|
||||
|
||||
// The flag is used at startup, so any change requires a restart
|
||||
RequiresRestart bool `json:"requiresRestart,omitempty"`
|
||||
|
||||
// Allow cloud users to set the values in UI
|
||||
AllowSelfServe bool `json:"allowSelfServe,omitempty"`
|
||||
|
||||
// Do not show the value in the UI
|
||||
HideFromAdminPage bool `json:"hideFromAdminPage,omitempty"`
|
||||
|
||||
// Do not show the value in docs
|
||||
HideFromDocs bool `json:"hideFromDocs,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FeatureList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []Feature `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// FeatureToggles define the feature state
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FeatureToggles struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// The configured toggles. Note this may include unknown fields
|
||||
Spec map[string]bool `json:"spec"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type FeatureTogglesList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []FeatureToggles `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type ResolvedToggleState struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// Can any flag be updated
|
||||
Writeable bool `json:"writeable,omitempty"`
|
||||
|
||||
// The currently enabled flags
|
||||
Enabled map[string]bool `json:"enabled,omitempty"`
|
||||
|
||||
// Details on the current status
|
||||
Toggles []ToggleStatus `json:"toggles,omitempty"`
|
||||
}
|
||||
|
||||
type ToggleStatus struct {
|
||||
// The feature toggle name
|
||||
Name string `json:"name"`
|
||||
|
||||
// The flag description
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// Is the flag enabled
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// Can this flag be updated
|
||||
Writeable bool `json:"writeable,omitempty"`
|
||||
|
||||
// Where was the value configured
|
||||
// eg: startup | tenant|org | user | browser
|
||||
// missing means default
|
||||
Source *common.ObjectReference `json:"source,omitempty"`
|
||||
|
||||
// eg: unknown flag
|
||||
Warning string `json:"warning,omitempty"`
|
||||
}
|
215
pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go
Normal file
215
pkg/apis/featuretoggle/v0alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,215 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
commonv0alpha1 "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Feature) DeepCopyInto(out *Feature) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Feature.
|
||||
func (in *Feature) DeepCopy() *Feature {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Feature)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Feature) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FeatureList) DeepCopyInto(out *FeatureList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Feature, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureList.
|
||||
func (in *FeatureList) DeepCopy() *FeatureList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FeatureList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FeatureList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FeatureSpec) DeepCopyInto(out *FeatureSpec) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureSpec.
|
||||
func (in *FeatureSpec) DeepCopy() *FeatureSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FeatureSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FeatureToggles) DeepCopyInto(out *FeatureToggles) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
if in.Spec != nil {
|
||||
in, out := &in.Spec, &out.Spec
|
||||
*out = make(map[string]bool, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureToggles.
|
||||
func (in *FeatureToggles) DeepCopy() *FeatureToggles {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FeatureToggles)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FeatureToggles) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FeatureTogglesList) DeepCopyInto(out *FeatureTogglesList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]FeatureToggles, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureTogglesList.
|
||||
func (in *FeatureTogglesList) DeepCopy() *FeatureTogglesList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FeatureTogglesList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *FeatureTogglesList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResolvedToggleState) DeepCopyInto(out *ResolvedToggleState) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Enabled != nil {
|
||||
in, out := &in.Enabled, &out.Enabled
|
||||
*out = make(map[string]bool, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Toggles != nil {
|
||||
in, out := &in.Toggles, &out.Toggles
|
||||
*out = make([]ToggleStatus, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolvedToggleState.
|
||||
func (in *ResolvedToggleState) DeepCopy() *ResolvedToggleState {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResolvedToggleState)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ResolvedToggleState) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ToggleStatus) DeepCopyInto(out *ToggleStatus) {
|
||||
*out = *in
|
||||
if in.Source != nil {
|
||||
in, out := &in.Source, &out.Source
|
||||
*out = new(commonv0alpha1.ObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ToggleStatus.
|
||||
func (in *ToggleStatus) DeepCopy() *ToggleStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ToggleStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
19
pkg/apis/featuretoggle/v0alpha1/zz_generated.defaults.go
Normal file
19
pkg/apis/featuretoggle/v0alpha1/zz_generated.defaults.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by defaulter-gen. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// RegisterDefaults adds defaulters functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
// All generated defaulters are covering - they call all nested defaulters.
|
||||
func RegisterDefaults(scheme *runtime.Scheme) error {
|
||||
return nil
|
||||
}
|
2899
pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go
Normal file
2899
pkg/apis/featuretoggle/v0alpha1/zz_generated.openapi.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,12 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
grafanaAPIServer "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -47,6 +51,12 @@ func (o *APIServerOptions) loadAPIGroupBuilders(args []string) error {
|
||||
// No dependencies for testing
|
||||
case "example.grafana.app":
|
||||
o.builders = append(o.builders, example.NewTestingAPIBuilder())
|
||||
case "featuretoggle.grafana.app":
|
||||
features, err := featuremgmt.ProvideManagerService(&setting.Cfg{}, &licensing.OSSLicensingService{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.builders = append(o.builders, featuretoggle.NewFeatureFlagAPIBuilder(features))
|
||||
case "testdata.datasource.grafana.app":
|
||||
ds, err := datasource.NewStandaloneDatasource(g)
|
||||
if err != nil {
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||
)
|
||||
@@ -23,6 +24,7 @@ func ProvideRegistryServiceSink(
|
||||
_ *dashboard.DashboardsAPIBuilder,
|
||||
_ *playlist.PlaylistAPIBuilder,
|
||||
_ *example.TestingAPIBuilder,
|
||||
_ *featuretoggle.FeatureFlagAPIBuilder,
|
||||
_ *datasource.DataSourceAPIBuilder,
|
||||
_ *folders.FolderAPIBuilder,
|
||||
) *Service {
|
||||
|
112
pkg/registry/apis/featuretoggle/current.go
Normal file
112
pkg/registry/apis/featuretoggle/current.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func getResolvedToggleState(ctx context.Context, features *featuremgmt.FeatureManager) v0alpha1.ResolvedToggleState {
|
||||
state := v0alpha1.ResolvedToggleState{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: v0alpha1.APIVERSION,
|
||||
Kind: "ResolvedToggleState",
|
||||
},
|
||||
Enabled: features.GetEnabled(ctx),
|
||||
}
|
||||
|
||||
// Reference to the object that defined the values
|
||||
startupRef := &common.ObjectReference{
|
||||
Namespace: "system",
|
||||
Name: "startup",
|
||||
}
|
||||
|
||||
startup := features.GetStartupFlags()
|
||||
warnings := features.GetWarning()
|
||||
for _, f := range features.GetFlags() {
|
||||
name := f.Name
|
||||
if features.IsHiddenFromAdminPage(name, true) {
|
||||
continue
|
||||
}
|
||||
|
||||
toggle := v0alpha1.ToggleStatus{
|
||||
Name: name,
|
||||
Description: f.Description, // simplify the UI changes
|
||||
Enabled: state.Enabled[name],
|
||||
Writeable: features.IsEditableFromAdminPage(name),
|
||||
Source: startupRef,
|
||||
Warning: warnings[name],
|
||||
}
|
||||
if f.Expression == "true" && toggle.Enabled {
|
||||
toggle.Source = nil
|
||||
}
|
||||
_, inStartup := startup[name]
|
||||
if toggle.Enabled || toggle.Writeable || toggle.Warning != "" || inStartup {
|
||||
state.Toggles = append(state.Toggles, toggle)
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) handleCurrentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPatch {
|
||||
b.handlePatchCurrent(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
state := getResolvedToggleState(r.Context(), b.features)
|
||||
|
||||
err := json.NewEncoder(w).Encode(state)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: authz is already handled by the authorizer
|
||||
func (b *FeatureFlagAPIBuilder) handlePatchCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
if !b.features.IsFeatureEditingAllowed() {
|
||||
_, _ = w.Write([]byte("Feature editing is disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
request := v0alpha1.ResolvedToggleState{}
|
||||
err := web.Bind(r, &request)
|
||||
if err != nil {
|
||||
_, _ = w.Write([]byte("ERROR!!! " + err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if len(request.Toggles) > 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("can only patch the enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
changes := map[string]bool{}
|
||||
for k, v := range request.Enabled {
|
||||
current := b.features.IsEnabled(ctx, k)
|
||||
if current != v {
|
||||
if !b.features.IsEditableFromAdminPage(k) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("can not edit toggle: " + k))
|
||||
return
|
||||
}
|
||||
changes[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte("TODO... actually UPDATE/call webhook: "))
|
||||
}
|
125
pkg/registry/apis/featuretoggle/features.go
Normal file
125
pkg/registry/apis/featuretoggle/features.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Storage = (*featuresStorage)(nil)
|
||||
_ rest.Scoper = (*featuresStorage)(nil)
|
||||
_ rest.SingularNameProvider = (*featuresStorage)(nil)
|
||||
_ rest.Lister = (*featuresStorage)(nil)
|
||||
_ rest.Getter = (*featuresStorage)(nil)
|
||||
)
|
||||
|
||||
type featuresStorage struct {
|
||||
resource *common.ResourceInfo
|
||||
tableConverter rest.TableConvertor
|
||||
features []featuremgmt.FeatureFlag
|
||||
startup int64
|
||||
}
|
||||
|
||||
// NOTE! this does not depend on config or any system state!
|
||||
// In the future, the existence of features (and their properties) can be defined dynamically
|
||||
func NewFeaturesStorage(features []featuremgmt.FeatureFlag) *featuresStorage {
|
||||
resourceInfo := v0alpha1.FeatureResourceInfo
|
||||
return &featuresStorage{
|
||||
startup: time.Now().UnixMilli(),
|
||||
resource: &resourceInfo,
|
||||
features: features,
|
||||
tableConverter: utils.NewTableConverter(
|
||||
resourceInfo.GroupResource(),
|
||||
[]metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Stage", Type: "string", Format: "string", Description: "Where is the flag in the dev cycle"},
|
||||
{Name: "Owner", Type: "string", Format: "string", Description: "Which team owns the feature"},
|
||||
},
|
||||
func(obj any) ([]interface{}, error) {
|
||||
r, ok := obj.(*v0alpha1.Feature)
|
||||
if ok {
|
||||
return []interface{}{
|
||||
r.Name,
|
||||
r.Spec.Stage,
|
||||
r.Spec.Owner,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("expected resource or info")
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *featuresStorage) New() runtime.Object {
|
||||
return s.resource.NewFunc()
|
||||
}
|
||||
|
||||
func (s *featuresStorage) Destroy() {}
|
||||
|
||||
func (s *featuresStorage) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *featuresStorage) GetSingularName() string {
|
||||
return s.resource.GetSingularName()
|
||||
}
|
||||
|
||||
func (s *featuresStorage) NewList() runtime.Object {
|
||||
return s.resource.NewListFunc()
|
||||
}
|
||||
|
||||
func (s *featuresStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *featuresStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
flags := &v0alpha1.FeatureList{
|
||||
ListMeta: metav1.ListMeta{
|
||||
ResourceVersion: fmt.Sprintf("%d", s.startup),
|
||||
},
|
||||
}
|
||||
for _, flag := range s.features {
|
||||
flags.Items = append(flags.Items, toK8sForm(flag))
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func (s *featuresStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
for _, flag := range s.features {
|
||||
if name == flag.Name {
|
||||
obj := toK8sForm(flag)
|
||||
return &obj, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
func toK8sForm(flag featuremgmt.FeatureFlag) v0alpha1.Feature {
|
||||
return v0alpha1.Feature{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: flag.Name,
|
||||
CreationTimestamp: metav1.NewTime(flag.Created),
|
||||
},
|
||||
Spec: v0alpha1.FeatureSpec{
|
||||
Description: flag.Description,
|
||||
Stage: flag.Stage.String(),
|
||||
Owner: string(flag.Owner),
|
||||
AllowSelfServe: flag.AllowSelfServe,
|
||||
HideFromAdminPage: flag.HideFromAdminPage,
|
||||
HideFromDocs: flag.HideFromDocs,
|
||||
FrontendOnly: flag.FrontendOnly,
|
||||
RequiresDevMode: flag.RequiresDevMode,
|
||||
RequiresRestart: flag.RequiresRestart,
|
||||
},
|
||||
}
|
||||
}
|
209
pkg/registry/apis/featuretoggle/register.go
Normal file
209
pkg/registry/apis/featuretoggle/register.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||
)
|
||||
|
||||
var _ grafanaapiserver.APIGroupBuilder = (*FeatureFlagAPIBuilder)(nil)
|
||||
|
||||
var gv = v0alpha1.SchemeGroupVersion
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type FeatureFlagAPIBuilder struct {
|
||||
features *featuremgmt.FeatureManager
|
||||
}
|
||||
|
||||
func NewFeatureFlagAPIBuilder(features *featuremgmt.FeatureManager) *FeatureFlagAPIBuilder {
|
||||
return &FeatureFlagAPIBuilder{features}
|
||||
}
|
||||
|
||||
func RegisterAPIService(features *featuremgmt.FeatureManager,
|
||||
apiregistration grafanaapiserver.APIRegistrar,
|
||||
) *FeatureFlagAPIBuilder {
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil // skip registration unless opting into experimental apis
|
||||
}
|
||||
builder := NewFeatureFlagAPIBuilder(features)
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||
return gv
|
||||
}
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
|
||||
scheme.AddKnownTypes(gv,
|
||||
&v0alpha1.Feature{},
|
||||
&v0alpha1.FeatureList{},
|
||||
&v0alpha1.FeatureToggles{},
|
||||
&v0alpha1.FeatureTogglesList{},
|
||||
&v0alpha1.ResolvedToggleState{},
|
||||
)
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
addKnownTypes(scheme, gv)
|
||||
|
||||
// Link this version to the internal representation.
|
||||
// This is used for server-side-apply (PATCH), and avoids the error:
|
||||
// "no kind is registered for the type"
|
||||
addKnownTypes(scheme, schema.GroupVersion{
|
||||
Group: gv.Group,
|
||||
Version: runtime.APIVersionInternal,
|
||||
})
|
||||
|
||||
// If multiple versions exist, then register conversions from zz_generated.conversion.go
|
||||
// if err := playlist.RegisterConversions(scheme); err != nil {
|
||||
// return err
|
||||
// }
|
||||
metav1.AddToGroupVersion(scheme, gv)
|
||||
return scheme.SetVersionPriority(gv)
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) GetAPIGroupInfo(
|
||||
scheme *runtime.Scheme,
|
||||
codecs serializer.CodecFactory, // pointer?
|
||||
optsGetter generic.RESTOptionsGetter,
|
||||
) (*genericapiserver.APIGroupInfo, error) {
|
||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs)
|
||||
|
||||
featureStore := NewFeaturesStorage(b.features.GetFlags())
|
||||
toggleStore := NewTogglesStorage(b.features)
|
||||
|
||||
storage := map[string]rest.Storage{}
|
||||
storage[featureStore.resource.StoragePath()] = featureStore
|
||||
storage[toggleStore.resource.StoragePath()] = toggleStore
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage
|
||||
return &apiGroupInfo, nil
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||
return v0alpha1.GetOpenAPIDefinitions
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return nil // default authorizer is fine
|
||||
}
|
||||
|
||||
// Register additional routes with the server
|
||||
func (b *FeatureFlagAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes {
|
||||
defs := v0alpha1.GetOpenAPIDefinitions(func(path string) spec.Ref { return spec.Ref{} })
|
||||
stateSchema := defs["github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1.ResolvedToggleState"].Schema
|
||||
|
||||
tags := []string{"Editor"}
|
||||
return &grafanaapiserver.APIRoutes{
|
||||
Root: []grafanaapiserver.APIRouteHandler{
|
||||
{
|
||||
Path: "current",
|
||||
Spec: &spec3.PathProps{
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
Tags: tags,
|
||||
Summary: "Current configuration with details",
|
||||
Description: "Show details about the current flags and where they come from",
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
StatusCodeResponses: map[int]*spec3.Response{
|
||||
200: {
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &stateSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
Description: "OK",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
Tags: tags,
|
||||
Summary: "Update individual toggles",
|
||||
Description: "Patch some of the toggles (keyed by the toggle name)",
|
||||
RequestBody: &spec3.RequestBody{
|
||||
RequestBodyProps: spec3.RequestBodyProps{
|
||||
Required: true,
|
||||
Description: "flags to change",
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &stateSchema,
|
||||
Example: &v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
featuremgmt.FlagAutoMigrateOldPanels: true,
|
||||
featuremgmt.FlagAngularDeprecationUI: false,
|
||||
},
|
||||
},
|
||||
Examples: map[string]*spec3.Example{
|
||||
"enable-auto-migrate": {
|
||||
ExampleProps: spec3.ExampleProps{
|
||||
Summary: "enable auto-migrate panels",
|
||||
Description: "example descr",
|
||||
Value: &v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
featuremgmt.FlagAutoMigrateOldPanels: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"disable-auto-migrate": {
|
||||
ExampleProps: spec3.ExampleProps{
|
||||
Summary: "disable auto-migrate panels",
|
||||
Description: "disable descr",
|
||||
Value: &v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
featuremgmt.FlagAutoMigrateOldPanels: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
StatusCodeResponses: map[int]*spec3.Response{
|
||||
200: {
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {},
|
||||
},
|
||||
Description: "OK",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: b.handleCurrentStatus,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
92
pkg/registry/apis/featuretoggle/toggles.go
Normal file
92
pkg/registry/apis/featuretoggle/toggles.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Storage = (*togglesStorage)(nil)
|
||||
_ rest.Scoper = (*togglesStorage)(nil)
|
||||
_ rest.SingularNameProvider = (*togglesStorage)(nil)
|
||||
_ rest.Lister = (*togglesStorage)(nil)
|
||||
_ rest.Getter = (*togglesStorage)(nil)
|
||||
)
|
||||
|
||||
type togglesStorage struct {
|
||||
resource *common.ResourceInfo
|
||||
tableConverter rest.TableConvertor
|
||||
|
||||
// The startup toggles
|
||||
startup *v0alpha1.FeatureToggles
|
||||
}
|
||||
|
||||
func NewTogglesStorage(features *featuremgmt.FeatureManager) *togglesStorage {
|
||||
resourceInfo := v0alpha1.TogglesResourceInfo
|
||||
return &togglesStorage{
|
||||
resource: &resourceInfo,
|
||||
startup: &v0alpha1.FeatureToggles{
|
||||
TypeMeta: resourceInfo.TypeMeta(),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "startup",
|
||||
Namespace: "system",
|
||||
CreationTimestamp: metav1.Now(),
|
||||
},
|
||||
Spec: features.GetStartupFlags(),
|
||||
},
|
||||
tableConverter: rest.NewDefaultTableConvertor(resourceInfo.GroupResource()),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *togglesStorage) New() runtime.Object {
|
||||
return s.resource.NewFunc()
|
||||
}
|
||||
|
||||
func (s *togglesStorage) Destroy() {}
|
||||
|
||||
func (s *togglesStorage) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *togglesStorage) GetSingularName() string {
|
||||
return s.resource.GetSingularName()
|
||||
}
|
||||
|
||||
func (s *togglesStorage) NewList() runtime.Object {
|
||||
return s.resource.NewListFunc()
|
||||
}
|
||||
|
||||
func (s *togglesStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
func (s *togglesStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
flags := &v0alpha1.FeatureTogglesList{
|
||||
Items: []v0alpha1.FeatureToggles{*s.startup},
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func (s *togglesStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
info, err := request.NamespaceInfoFrom(ctx, false) // allow system
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.Value != "" && info.Value != "system" {
|
||||
return nil, fmt.Errorf("only system namespace is currently supported")
|
||||
}
|
||||
if name != "startup" {
|
||||
return nil, fmt.Errorf("only system/startup is currently supported")
|
||||
}
|
||||
return s.startup, nil
|
||||
}
|
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||
)
|
||||
@@ -17,6 +18,7 @@ var WireSet = wire.NewSet(
|
||||
playlist.RegisterAPIService,
|
||||
dashboard.RegisterAPIService,
|
||||
example.RegisterAPIService,
|
||||
featuretoggle.RegisterAPIService,
|
||||
datasource.RegisterAPIService,
|
||||
folders.RegisterAPIService,
|
||||
)
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -16,13 +17,15 @@ var (
|
||||
type FeatureManager struct {
|
||||
isDevMod bool
|
||||
restartRequired bool
|
||||
allowEditing bool
|
||||
licensing licensing.Licensing
|
||||
flags map[string]*FeatureFlag
|
||||
enabled map[string]bool // only the "on" values
|
||||
startup map[string]bool // the explicit values registered at startup
|
||||
warnings map[string]string // potential warnings about the flag
|
||||
log log.Logger
|
||||
|
||||
Settings setting.FeatureMgmtSettings
|
||||
|
||||
licensing licensing.Licensing
|
||||
flags map[string]*FeatureFlag
|
||||
enabled map[string]bool // only the "on" values
|
||||
startup map[string]bool // the explicit values registered at startup
|
||||
warnings map[string]string // potential warnings about the flag
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// This will merge the flags with the current configuration
|
||||
@@ -141,22 +144,66 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag {
|
||||
}
|
||||
|
||||
func (fm *FeatureManager) GetState() *FeatureManagerState {
|
||||
return &FeatureManagerState{RestartRequired: fm.restartRequired, AllowEditing: fm.allowEditing}
|
||||
return &FeatureManagerState{
|
||||
RestartRequired: fm.restartRequired,
|
||||
AllowEditing: fm.Settings.AllowEditing,
|
||||
}
|
||||
}
|
||||
|
||||
// isFeatureEditingAllowed checks if the backend is properly configured to allow feature toggle changes from the UI
|
||||
func (fm *FeatureManager) IsFeatureEditingAllowed() bool {
|
||||
return fm.Settings.AllowEditing && fm.Settings.UpdateWebhook != ""
|
||||
}
|
||||
|
||||
// Flags that can be edited
|
||||
func (fm *FeatureManager) IsEditableFromAdminPage(key string) bool {
|
||||
flag, ok := fm.flags[key]
|
||||
if !ok ||
|
||||
!fm.IsFeatureEditingAllowed() ||
|
||||
!flag.AllowSelfServe ||
|
||||
flag.Name == FlagFeatureToggleAdminPage {
|
||||
return false
|
||||
}
|
||||
return flag.Stage == FeatureStageGeneralAvailability ||
|
||||
flag.Stage == FeatureStageDeprecated
|
||||
}
|
||||
|
||||
// Flags that should not be shown in the UI (regardless of their state)
|
||||
func (fm *FeatureManager) IsHiddenFromAdminPage(key string, lenient bool) bool {
|
||||
_, hide := fm.Settings.HiddenToggles[key]
|
||||
flag, ok := fm.flags[key]
|
||||
if !ok || flag.HideFromAdminPage || hide {
|
||||
return true // unknown flag (should we show it as a warning!)
|
||||
}
|
||||
|
||||
// Explicitly hidden from configs
|
||||
_, found := fm.Settings.HiddenToggles[key]
|
||||
if found {
|
||||
return true
|
||||
}
|
||||
if lenient {
|
||||
return false
|
||||
}
|
||||
|
||||
return flag.Stage == FeatureStageUnknown ||
|
||||
flag.Stage == FeatureStageExperimental ||
|
||||
flag.Stage == FeatureStagePrivatePreview
|
||||
}
|
||||
|
||||
// Get the flags that were explicitly set on startup
|
||||
func (fm *FeatureManager) GetStartupFlags() map[string]bool {
|
||||
return fm.startup
|
||||
}
|
||||
|
||||
// Perhaps expose the flag warnings
|
||||
func (fm *FeatureManager) GetWarning() map[string]string {
|
||||
return fm.warnings
|
||||
}
|
||||
|
||||
func (fm *FeatureManager) SetRestartRequired() {
|
||||
fm.restartRequired = true
|
||||
}
|
||||
|
||||
// Check to see if a feature toggle exists by name
|
||||
func (fm *FeatureManager) LookupFlag(name string) (FeatureFlag, bool) {
|
||||
f, ok := fm.flags[name]
|
||||
if !ok {
|
||||
return FeatureFlag{}, false
|
||||
}
|
||||
return *f, true
|
||||
}
|
||||
|
||||
// ############# Test Functions #############
|
||||
|
||||
func WithFeatures(spec ...any) FeatureToggles {
|
||||
@@ -193,7 +240,7 @@ func WithManager(spec ...any) *FeatureManager {
|
||||
// WithFeatureManager is used to define feature toggle manager for testing.
|
||||
// It should be used when your test feature toggles require metadata beyond `Name` and `Enabled`.
|
||||
// You should provide a feature toggle Name at a minimum.
|
||||
func WithFeatureManager(flags []*FeatureFlag, disabled ...string) *FeatureManager {
|
||||
func WithFeatureManager(cfg setting.FeatureMgmtSettings, flags []*FeatureFlag, disabled ...string) *FeatureManager {
|
||||
count := len(flags)
|
||||
features := make(map[string]*FeatureFlag, count)
|
||||
enabled := make(map[string]bool, count)
|
||||
@@ -211,5 +258,11 @@ func WithFeatureManager(flags []*FeatureFlag, disabled ...string) *FeatureManage
|
||||
enabled[f.Name] = !dis[f.Name]
|
||||
}
|
||||
|
||||
return &FeatureManager{enabled: enabled, flags: features, startup: enabled, warnings: map[string]string{}}
|
||||
return &FeatureManager{
|
||||
Settings: cfg,
|
||||
enabled: enabled,
|
||||
flags: features,
|
||||
startup: enabled,
|
||||
warnings: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
@@ -1277,6 +1277,16 @@ var (
|
||||
AllowSelfServe: false,
|
||||
Created: time.Date(2023, time.December, 18, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "kubernetesFeatureToggles",
|
||||
Description: "Use the kubernetes API for feature toggle management in the frontend",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaOperatorExperienceSquad,
|
||||
AllowSelfServe: false,
|
||||
Created: time.Date(2023, time.December, 22, 3, 43, 0, 0, time.UTC),
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
{
|
||||
Name: "alertingPreviewUpgrade",
|
||||
Description: "Show Unified Alerting preview and upgrade page in legacy alerting",
|
||||
|
@@ -20,14 +20,14 @@ var (
|
||||
|
||||
func ProvideManagerService(cfg *setting.Cfg, licensing licensing.Licensing) (*FeatureManager, error) {
|
||||
mgmt := &FeatureManager{
|
||||
isDevMod: setting.Env != setting.Prod,
|
||||
licensing: licensing,
|
||||
flags: make(map[string]*FeatureFlag, 30),
|
||||
enabled: make(map[string]bool),
|
||||
startup: make(map[string]bool),
|
||||
warnings: make(map[string]string),
|
||||
allowEditing: cfg.FeatureManagement.AllowEditing && cfg.FeatureManagement.UpdateWebhook != "",
|
||||
log: log.New("featuremgmt"),
|
||||
isDevMod: setting.Env != setting.Prod,
|
||||
licensing: licensing,
|
||||
flags: make(map[string]*FeatureFlag, 30),
|
||||
enabled: make(map[string]bool),
|
||||
startup: make(map[string]bool),
|
||||
warnings: make(map[string]string),
|
||||
Settings: cfg.FeatureManagement,
|
||||
log: log.New("featuremgmt"),
|
||||
}
|
||||
|
||||
// Register the standard flags
|
||||
|
@@ -150,6 +150,7 @@ regressionTransformation,preview,@grafana/grafana-bi-squad,2023-11-24,false,fals
|
||||
displayAnonymousStats,GA,@grafana/identity-access-team,2023-11-29,false,false,false,true
|
||||
alertStateHistoryAnnotationsFromLoki,experimental,@grafana/alerting-squad,2023-11-30,false,false,true,false
|
||||
lokiQueryHints,GA,@grafana/observability-logs,2023-12-18,false,false,false,true
|
||||
kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,2023-12-22,false,false,false,true
|
||||
alertingPreviewUpgrade,experimental,@grafana/alerting-squad,2024-01-03,false,false,true,false
|
||||
enablePluginsTracingByDefault,experimental,@grafana/plugins-platform-backend,2024-01-09,false,false,true,false
|
||||
cloudRBACRoles,experimental,@grafana/identity-access-team,2024-01-10,false,false,true,false
|
||||
|
|
@@ -611,6 +611,10 @@ const (
|
||||
// Enables query hints for Loki
|
||||
FlagLokiQueryHints = "lokiQueryHints"
|
||||
|
||||
// FlagKubernetesFeatureToggles
|
||||
// Use the kubernetes API for feature toggle management in the frontend
|
||||
FlagKubernetesFeatureToggles = "kubernetesFeatureToggles"
|
||||
|
||||
// FlagAlertingPreviewUpgrade
|
||||
// Show Unified Alerting preview and upgrade page in legacy alerting
|
||||
FlagAlertingPreviewUpgrade = "alertingPreviewUpgrade"
|
||||
|
Reference in New Issue
Block a user