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 |
|
| `flameGraphItemCollapsing` | Allow collapsing of flame graph items |
|
||||||
| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes |
|
| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes |
|
||||||
| `tableSharedCrosshair` | Enables shared crosshair in table panel |
|
| `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 |
|
| `enablePluginsTracingByDefault` | Enable plugin tracing for all external plugins |
|
||||||
| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled |
|
| `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
|
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:
|
The current workflow (sorry!) is to:
|
||||||
|
|
||||||
1. update the script to point to the group+version you want
|
1. update the script to point to the group+version you want
|
||||||
|
@@ -169,6 +169,7 @@ export interface FeatureToggles {
|
|||||||
displayAnonymousStats?: boolean;
|
displayAnonymousStats?: boolean;
|
||||||
alertStateHistoryAnnotationsFromLoki?: boolean;
|
alertStateHistoryAnnotationsFromLoki?: boolean;
|
||||||
lokiQueryHints?: boolean;
|
lokiQueryHints?: boolean;
|
||||||
|
kubernetesFeatureToggles?: boolean;
|
||||||
alertingPreviewUpgrade?: boolean;
|
alertingPreviewUpgrade?: boolean;
|
||||||
enablePluginsTracingByDefault?: boolean;
|
enablePluginsTracingByDefault?: boolean;
|
||||||
cloudRBACRoles?: boolean;
|
cloudRBACRoles?: boolean;
|
||||||
|
@@ -18,22 +18,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) GetFeatureToggles(ctx *contextmodel.ReqContext) response.Response {
|
||||||
cfg := hs.Cfg.FeatureManagement
|
|
||||||
enabledFeatures := hs.Features.GetEnabled(ctx.Req.Context())
|
|
||||||
|
|
||||||
// object being returned
|
// object being returned
|
||||||
dtos := make([]featuremgmt.FeatureToggleDTO, 0)
|
dtos := make([]featuremgmt.FeatureToggleDTO, 0)
|
||||||
|
|
||||||
// loop through features an add features that should be visible to dtos
|
// loop through features an add features that should be visible to dtos
|
||||||
for _, ft := range hs.featureManager.GetFlags() {
|
for _, ft := range hs.featureManager.GetFlags() {
|
||||||
if isFeatureHidden(ft, cfg.HiddenToggles) {
|
flag := ft.Name
|
||||||
|
if hs.featureManager.IsHiddenFromAdminPage(flag, false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dto := featuremgmt.FeatureToggleDTO{
|
dto := featuremgmt.FeatureToggleDTO{
|
||||||
Name: ft.Name,
|
Name: flag,
|
||||||
Description: ft.Description,
|
Description: ft.Description,
|
||||||
Enabled: enabledFeatures[ft.Name],
|
Enabled: hs.featureManager.IsEnabled(ctx.Req.Context(), flag),
|
||||||
ReadOnly: !isFeatureWriteable(ft, cfg.ReadOnlyToggles) || !isFeatureEditingAllowed(*hs.Cfg),
|
ReadOnly: !hs.featureManager.IsEditableFromAdminPage(flag),
|
||||||
}
|
}
|
||||||
|
|
||||||
dtos = append(dtos, dto)
|
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 {
|
func (hs *HTTPServer) UpdateFeatureToggle(ctx *contextmodel.ReqContext) response.Response {
|
||||||
featureMgmtCfg := hs.Cfg.FeatureManagement
|
featureMgmtCfg := hs.featureManager.Settings
|
||||||
if !featureMgmtCfg.AllowEditing {
|
if !featureMgmtCfg.AllowEditing {
|
||||||
return response.Error(http.StatusForbidden, "feature toggles are read-only", fmt.Errorf("feature toggles are configured to be read-only"))
|
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 {
|
for _, t := range cmd.FeatureToggles {
|
||||||
// make sure flag exists, and only continue if flag is writeable
|
// 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)
|
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)
|
payload.FeatureToggles[t.Name] = strconv.FormatBool(t.Enabled)
|
||||||
} else {
|
} else {
|
||||||
@@ -92,32 +90,6 @@ func (hs *HTTPServer) GetFeatureMgmtState(ctx *contextmodel.ReqContext) response
|
|||||||
return response.Respond(http.StatusOK, fmState)
|
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 {
|
type UpdatePayload struct {
|
||||||
FeatureToggles map[string]string `json:"feature_toggles"`
|
FeatureToggles map[string]string `json:"feature_toggles"`
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
|
@@ -393,9 +393,8 @@ func runGetScenario(
|
|||||||
) []featuremgmt.FeatureToggleDTO {
|
) []featuremgmt.FeatureToggleDTO {
|
||||||
// Set up server and send request
|
// Set up server and send request
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.FeatureManagement = settings
|
|
||||||
|
|
||||||
fm := featuremgmt.WithFeatureManager(append([]*featuremgmt.FeatureFlag{{
|
fm := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{
|
||||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||||
}}, features...), disabled...)
|
}}, features...), disabled...)
|
||||||
@@ -460,9 +459,7 @@ func runSetScenario(
|
|||||||
) *http.Response {
|
) *http.Response {
|
||||||
// Set up server and send request
|
// Set up server and send request
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.FeatureManagement = settings
|
features := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{
|
||||||
|
|
||||||
features := featuremgmt.WithFeatureManager(append([]*featuremgmt.FeatureFlag{{
|
|
||||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||||
}}, serverFeatures...), disabled...)
|
}}, 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 {
|
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||||
return map[string]common.OpenAPIDefinition{
|
return map[string]common.OpenAPIDefinition{
|
||||||
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(),
|
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.ObjectReference": schema_pkg_apis_common_v0alpha1_ObjectReference(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
|
"github.com/grafana/grafana/pkg/apis/common/v0alpha1.Unstructured": Unstructured{}.OpenAPIDefinition(),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref),
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref),
|
||||||
"k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref),
|
"k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref),
|
||||||
"k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref),
|
"k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref),
|
||||||
"k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref),
|
"k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref),
|
||||||
"k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(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/datasource"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
"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"
|
grafanaAPIServer "github.com/grafana/grafana/pkg/services/grafana-apiserver"
|
||||||
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
|
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
|
||||||
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -47,6 +51,12 @@ func (o *APIServerOptions) loadAPIGroupBuilders(args []string) error {
|
|||||||
// No dependencies for testing
|
// No dependencies for testing
|
||||||
case "example.grafana.app":
|
case "example.grafana.app":
|
||||||
o.builders = append(o.builders, example.NewTestingAPIBuilder())
|
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":
|
case "testdata.datasource.grafana.app":
|
||||||
ds, err := datasource.NewStandaloneDatasource(g)
|
ds, err := datasource.NewStandaloneDatasource(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
"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/folders"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||||
)
|
)
|
||||||
@@ -23,6 +24,7 @@ func ProvideRegistryServiceSink(
|
|||||||
_ *dashboard.DashboardsAPIBuilder,
|
_ *dashboard.DashboardsAPIBuilder,
|
||||||
_ *playlist.PlaylistAPIBuilder,
|
_ *playlist.PlaylistAPIBuilder,
|
||||||
_ *example.TestingAPIBuilder,
|
_ *example.TestingAPIBuilder,
|
||||||
|
_ *featuretoggle.FeatureFlagAPIBuilder,
|
||||||
_ *datasource.DataSourceAPIBuilder,
|
_ *datasource.DataSourceAPIBuilder,
|
||||||
_ *folders.FolderAPIBuilder,
|
_ *folders.FolderAPIBuilder,
|
||||||
) *Service {
|
) *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/dashboard"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/example"
|
"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/folders"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
"github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||||
)
|
)
|
||||||
@@ -17,6 +18,7 @@ var WireSet = wire.NewSet(
|
|||||||
playlist.RegisterAPIService,
|
playlist.RegisterAPIService,
|
||||||
dashboard.RegisterAPIService,
|
dashboard.RegisterAPIService,
|
||||||
example.RegisterAPIService,
|
example.RegisterAPIService,
|
||||||
|
featuretoggle.RegisterAPIService,
|
||||||
datasource.RegisterAPIService,
|
datasource.RegisterAPIService,
|
||||||
folders.RegisterAPIService,
|
folders.RegisterAPIService,
|
||||||
)
|
)
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -16,13 +17,15 @@ var (
|
|||||||
type FeatureManager struct {
|
type FeatureManager struct {
|
||||||
isDevMod bool
|
isDevMod bool
|
||||||
restartRequired bool
|
restartRequired bool
|
||||||
allowEditing bool
|
|
||||||
licensing licensing.Licensing
|
Settings setting.FeatureMgmtSettings
|
||||||
flags map[string]*FeatureFlag
|
|
||||||
enabled map[string]bool // only the "on" values
|
licensing licensing.Licensing
|
||||||
startup map[string]bool // the explicit values registered at startup
|
flags map[string]*FeatureFlag
|
||||||
warnings map[string]string // potential warnings about the flag
|
enabled map[string]bool // only the "on" values
|
||||||
log log.Logger
|
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
|
// This will merge the flags with the current configuration
|
||||||
@@ -141,22 +144,66 @@ func (fm *FeatureManager) GetFlags() []FeatureFlag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fm *FeatureManager) GetState() *FeatureManagerState {
|
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() {
|
func (fm *FeatureManager) SetRestartRequired() {
|
||||||
fm.restartRequired = true
|
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 #############
|
// ############# Test Functions #############
|
||||||
|
|
||||||
func WithFeatures(spec ...any) FeatureToggles {
|
func WithFeatures(spec ...any) FeatureToggles {
|
||||||
@@ -193,7 +240,7 @@ func WithManager(spec ...any) *FeatureManager {
|
|||||||
// WithFeatureManager is used to define feature toggle manager for testing.
|
// 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`.
|
// 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.
|
// 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)
|
count := len(flags)
|
||||||
features := make(map[string]*FeatureFlag, count)
|
features := make(map[string]*FeatureFlag, count)
|
||||||
enabled := make(map[string]bool, count)
|
enabled := make(map[string]bool, count)
|
||||||
@@ -211,5 +258,11 @@ func WithFeatureManager(flags []*FeatureFlag, disabled ...string) *FeatureManage
|
|||||||
enabled[f.Name] = !dis[f.Name]
|
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,
|
AllowSelfServe: false,
|
||||||
Created: time.Date(2023, time.December, 18, 12, 0, 0, 0, time.UTC),
|
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",
|
Name: "alertingPreviewUpgrade",
|
||||||
Description: "Show Unified Alerting preview and upgrade page in legacy alerting",
|
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) {
|
func ProvideManagerService(cfg *setting.Cfg, licensing licensing.Licensing) (*FeatureManager, error) {
|
||||||
mgmt := &FeatureManager{
|
mgmt := &FeatureManager{
|
||||||
isDevMod: setting.Env != setting.Prod,
|
isDevMod: setting.Env != setting.Prod,
|
||||||
licensing: licensing,
|
licensing: licensing,
|
||||||
flags: make(map[string]*FeatureFlag, 30),
|
flags: make(map[string]*FeatureFlag, 30),
|
||||||
enabled: make(map[string]bool),
|
enabled: make(map[string]bool),
|
||||||
startup: make(map[string]bool),
|
startup: make(map[string]bool),
|
||||||
warnings: make(map[string]string),
|
warnings: make(map[string]string),
|
||||||
allowEditing: cfg.FeatureManagement.AllowEditing && cfg.FeatureManagement.UpdateWebhook != "",
|
Settings: cfg.FeatureManagement,
|
||||||
log: log.New("featuremgmt"),
|
log: log.New("featuremgmt"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the standard flags
|
// 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
|
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
|
alertStateHistoryAnnotationsFromLoki,experimental,@grafana/alerting-squad,2023-11-30,false,false,true,false
|
||||||
lokiQueryHints,GA,@grafana/observability-logs,2023-12-18,false,false,false,true
|
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
|
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
|
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
|
cloudRBACRoles,experimental,@grafana/identity-access-team,2024-01-10,false,false,true,false
|
||||||
|
|
@@ -611,6 +611,10 @@ const (
|
|||||||
// Enables query hints for Loki
|
// Enables query hints for Loki
|
||||||
FlagLokiQueryHints = "lokiQueryHints"
|
FlagLokiQueryHints = "lokiQueryHints"
|
||||||
|
|
||||||
|
// FlagKubernetesFeatureToggles
|
||||||
|
// Use the kubernetes API for feature toggle management in the frontend
|
||||||
|
FlagKubernetesFeatureToggles = "kubernetesFeatureToggles"
|
||||||
|
|
||||||
// FlagAlertingPreviewUpgrade
|
// FlagAlertingPreviewUpgrade
|
||||||
// Show Unified Alerting preview and upgrade page in legacy alerting
|
// Show Unified Alerting preview and upgrade page in legacy alerting
|
||||||
FlagAlertingPreviewUpgrade = "alertingPreviewUpgrade"
|
FlagAlertingPreviewUpgrade = "alertingPreviewUpgrade"
|
||||||
|
Reference in New Issue
Block a user