FeatureToggles: Remove deprecated experimental apiserver (#111617)
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
|
||||
@@ -22,7 +21,6 @@ type Service struct{}
|
||||
func ProvideRegistryServiceSink(
|
||||
_ *dashboardinternal.DashboardsAPIBuilder,
|
||||
_ *dashboardsnapshot.SnapshotsAPIBuilder,
|
||||
_ *featuretoggle.FeatureFlagAPIBuilder,
|
||||
_ *datasource.DataSourceAPIBuilder,
|
||||
_ *folders.FolderAPIBuilder,
|
||||
_ *iam.IdentityAccessManagementAPIBuilder,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
This package supports the [Feature toggle admin page](https://grafana.com/docs/grafana/latest/administration/feature-toggles/) feature.
|
||||
|
||||
In order to update feature toggles through the app, the PATCH handler calls a webhook that should update Grafana's configuration and restarts the instance.
|
||||
|
||||
For local development, set the app mode to `development` by adding `app_mode = development` to the top level of your Grafana .ini file.
|
||||
@@ -1,237 +0,0 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errhttp"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) getResolvedToggleState(ctx context.Context) v0alpha1.ResolvedToggleState {
|
||||
state := v0alpha1.ResolvedToggleState{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: v0alpha1.APIVERSION,
|
||||
Kind: "ResolvedToggleState",
|
||||
},
|
||||
Enabled: b.features.GetEnabled(ctx),
|
||||
RestartRequired: b.features.IsRestartRequired(),
|
||||
}
|
||||
|
||||
// Reference to the object that defined the values
|
||||
startupRef := &common.ObjectReference{
|
||||
Namespace: "system",
|
||||
Name: "startup",
|
||||
}
|
||||
|
||||
startup := b.features.GetStartupFlags()
|
||||
warnings := b.features.GetWarning()
|
||||
for _, f := range b.features.GetFlags() {
|
||||
name := f.Name
|
||||
if b.features.IsHiddenFromAdminPage(name, false) {
|
||||
continue
|
||||
}
|
||||
|
||||
toggle := v0alpha1.ToggleStatus{
|
||||
Name: name,
|
||||
Description: f.Description, // simplify the UI changes
|
||||
Stage: f.Stage.String(),
|
||||
Enabled: state.Enabled[name],
|
||||
Writeable: b.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)
|
||||
}
|
||||
|
||||
if toggle.Writeable {
|
||||
state.AllowEditing = true
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the user can actually write values
|
||||
if state.AllowEditing {
|
||||
state.AllowEditing = b.features.IsFeatureEditingAllowed() && b.userCanWrite(ctx, nil)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) userCanRead(ctx context.Context, u identity.Requester) bool {
|
||||
if u == nil {
|
||||
u, _ = identity.GetRequester(ctx)
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
ok, err := b.accessControl.Evaluate(ctx, u, ac.EvalPermission(ac.ActionFeatureManagementRead))
|
||||
return ok && err == nil
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) userCanWrite(ctx context.Context, u identity.Requester) bool {
|
||||
if u == nil {
|
||||
u, _ = identity.GetRequester(ctx)
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
ok, err := b.accessControl.Evaluate(ctx, u, ac.EvalPermission(ac.ActionFeatureManagementWrite))
|
||||
return ok && err == nil
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) handleCurrentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPatch {
|
||||
b.handlePatchCurrent(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user can access toggle info
|
||||
ctx := r.Context()
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
if !b.userCanRead(ctx, user) {
|
||||
err = errutil.Unauthorized("featuretoggle.canNotRead",
|
||||
errutil.WithPublicMessage("missing read permission")).Errorf("user %s does not have read permissions", user.GetLogin())
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Write the state to the response body
|
||||
state := b.getResolvedToggleState(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(state)
|
||||
}
|
||||
|
||||
// NOTE: authz is already handled by the authorizer
|
||||
func (b *FeatureFlagAPIBuilder) handlePatchCurrent(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !b.features.IsFeatureEditingAllowed() {
|
||||
err := errutil.Forbidden("featuretoggle.disabled",
|
||||
errutil.WithPublicMessage("feature toggles are read-only")).Errorf("feature toggles are not writeable due to missing configuration")
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
if !b.userCanWrite(ctx, user) {
|
||||
err = errutil.Unauthorized("featuretoggle.canNotWrite",
|
||||
errutil.WithPublicMessage("missing write permission")).Errorf("user %s does not have write permissions", user.GetLogin())
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
request := v0alpha1.ResolvedToggleState{}
|
||||
err = web.Bind(r, &request)
|
||||
if err != nil {
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
if len(request.Toggles) > 0 {
|
||||
err = errutil.BadRequest("featuretoggle.badRequest",
|
||||
errutil.WithPublicMessage("can only patch the enabled section")).Errorf("request payload included properties in the read-only Toggles section")
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
changes := map[string]string{} // TODO would be nice to have this be a bool on the HG side
|
||||
for k, v := range request.Enabled {
|
||||
current := b.features.IsEnabled(ctx, k)
|
||||
if current != v {
|
||||
if !b.features.IsEditableFromAdminPage(k) {
|
||||
err = errutil.BadRequest("featuretoggle.badRequest",
|
||||
errutil.WithPublicMessage("invalid toggle passed in")).Errorf("can not edit toggle %s", k)
|
||||
errhttp.Write(ctx, err, w)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
changes[k] = strconv.FormatBool(v)
|
||||
}
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
payload := featuremgmt.FeatureToggleWebhookPayload{
|
||||
FeatureToggles: changes,
|
||||
User: user.GetEmail(),
|
||||
}
|
||||
|
||||
err = sendWebhookUpdate(b.features.Settings, payload)
|
||||
if err != nil && b.cfg.Env != setting.Dev {
|
||||
err = errutil.Internal("featuretoggle.webhookFailure", errutil.WithPublicMessage("an error occurred while updating feeature toggles")).Errorf("webhook error: %w", err)
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
b.features.SetRestartRequired()
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("feature toggles updated successfully"))
|
||||
}
|
||||
|
||||
func sendWebhookUpdate(cfg setting.FeatureMgmtSettings, payload featuremgmt.FeatureToggleWebhookPayload) error {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, cfg.UpdateWebhook, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.UpdateWebhookToken)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
if body, err := io.ReadAll(resp.Body); err != nil {
|
||||
return fmt.Errorf("SendWebhookUpdate failed with status=%d, error: %s", resp.StatusCode, string(body))
|
||||
} else {
|
||||
return fmt.Errorf("SendWebhookUpdate failed with status=%d, error: %w", resp.StatusCode, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetFeatureToggles(t *testing.T) {
|
||||
t.Run("fails without adequate permissions", func(t *testing.T) {
|
||||
features := featuremgmt.WithFeatureManager(setting.FeatureMgmtSettings{}, []*featuremgmt.FeatureFlag{{
|
||||
// Add this here to ensure the feature works as expected during tests
|
||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}})
|
||||
|
||||
b := NewFeatureFlagAPIBuilder(features, actest.FakeAccessControl{ExpectedEvaluate: false}, &setting.Cfg{})
|
||||
|
||||
callGetWith(t, b, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
t.Run("should be able to get feature toggles", func(t *testing.T) {
|
||||
features := []*featuremgmt.FeatureFlag{
|
||||
{
|
||||
Name: "toggle1",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}, {
|
||||
Name: "toggle2",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
},
|
||||
}
|
||||
disabled := []string{"toggle2"}
|
||||
|
||||
b := newTestAPIBuilder(t, features, disabled, setting.FeatureMgmtSettings{})
|
||||
result := callGetWith(t, b, http.StatusOK)
|
||||
assert.Len(t, result.Toggles, 2)
|
||||
t1, _ := findResult(t, result, "toggle1")
|
||||
assert.True(t, t1.Enabled)
|
||||
t2, _ := findResult(t, result, "toggle2")
|
||||
assert.False(t, t2.Enabled)
|
||||
})
|
||||
|
||||
t.Run("toggles hidden by config are not present in the response", func(t *testing.T) {
|
||||
features := []*featuremgmt.FeatureFlag{
|
||||
{
|
||||
Name: "toggle1",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}, {
|
||||
Name: "toggle2",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
},
|
||||
}
|
||||
settings := setting.FeatureMgmtSettings{
|
||||
HiddenToggles: map[string]struct{}{"toggle1": {}},
|
||||
}
|
||||
|
||||
b := newTestAPIBuilder(t, features, []string{}, settings)
|
||||
result := callGetWith(t, b, http.StatusOK)
|
||||
|
||||
assert.Len(t, result.Toggles, 1)
|
||||
assert.Equal(t, "toggle2", result.Toggles[0].Name)
|
||||
})
|
||||
|
||||
t.Run("toggles that are read-only by config have the readOnly field set", func(t *testing.T) {
|
||||
features := []*featuremgmt.FeatureFlag{
|
||||
{
|
||||
Name: "toggle1",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}, {
|
||||
Name: "toggle2",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
},
|
||||
}
|
||||
disabled := []string{"toggle2"}
|
||||
settings := setting.FeatureMgmtSettings{
|
||||
HiddenToggles: map[string]struct{}{"toggle1": {}},
|
||||
ReadOnlyToggles: map[string]struct{}{"toggle2": {}},
|
||||
AllowEditing: true,
|
||||
UpdateWebhook: "bogus",
|
||||
}
|
||||
|
||||
b := newTestAPIBuilder(t, features, disabled, settings)
|
||||
result := callGetWith(t, b, http.StatusOK)
|
||||
|
||||
assert.Len(t, result.Toggles, 1)
|
||||
assert.Equal(t, "toggle2", result.Toggles[0].Name)
|
||||
assert.False(t, result.Toggles[0].Writeable)
|
||||
})
|
||||
|
||||
t.Run("feature toggle defailts", func(t *testing.T) {
|
||||
features := []*featuremgmt.FeatureFlag{
|
||||
{
|
||||
Name: "toggle1",
|
||||
Stage: featuremgmt.FeatureStageUnknown,
|
||||
}, {
|
||||
Name: "toggle2",
|
||||
Stage: featuremgmt.FeatureStageExperimental,
|
||||
}, {
|
||||
Name: "toggle3",
|
||||
Stage: featuremgmt.FeatureStagePrivatePreview,
|
||||
}, {
|
||||
Name: "toggle4",
|
||||
Stage: featuremgmt.FeatureStagePublicPreview,
|
||||
AllowSelfServe: true,
|
||||
}, {
|
||||
Name: "toggle5",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
AllowSelfServe: true,
|
||||
}, {
|
||||
Name: "toggle6",
|
||||
Stage: featuremgmt.FeatureStageDeprecated,
|
||||
AllowSelfServe: true,
|
||||
}, {
|
||||
Name: "toggle7",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
AllowSelfServe: false,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("unknown, experimental, and private preview toggles are hidden by default", func(t *testing.T) {
|
||||
b := newTestAPIBuilder(t, features, []string{}, setting.FeatureMgmtSettings{})
|
||||
result := callGetWith(t, b, http.StatusOK)
|
||||
|
||||
assert.Len(t, result.Toggles, 4)
|
||||
|
||||
_, ok := findResult(t, result, "toggle1")
|
||||
assert.False(t, ok)
|
||||
_, ok = findResult(t, result, "toggle2")
|
||||
assert.False(t, ok)
|
||||
_, ok = findResult(t, result, "toggle3")
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("only public preview and GA with AllowSelfServe are writeable", func(t *testing.T) {
|
||||
settings := setting.FeatureMgmtSettings{
|
||||
AllowEditing: true,
|
||||
UpdateWebhook: "bogus",
|
||||
}
|
||||
|
||||
b := newTestAPIBuilder(t, features, []string{}, settings)
|
||||
result := callGetWith(t, b, http.StatusOK)
|
||||
|
||||
t4, ok := findResult(t, result, "toggle4")
|
||||
assert.True(t, ok)
|
||||
assert.True(t, t4.Writeable)
|
||||
t5, ok := findResult(t, result, "toggle5")
|
||||
assert.True(t, ok)
|
||||
assert.True(t, t5.Writeable)
|
||||
t6, ok := findResult(t, result, "toggle6")
|
||||
assert.True(t, ok)
|
||||
assert.True(t, t6.Writeable)
|
||||
})
|
||||
|
||||
t.Run("all toggles are read-only when server is misconfigured", func(t *testing.T) {
|
||||
settings := setting.FeatureMgmtSettings{
|
||||
AllowEditing: false,
|
||||
UpdateWebhook: "",
|
||||
}
|
||||
b := newTestAPIBuilder(t, features, []string{}, settings)
|
||||
result := callGetWith(t, b, http.StatusOK)
|
||||
|
||||
assert.Len(t, result.Toggles, 4)
|
||||
|
||||
t4, ok := findResult(t, result, "toggle4")
|
||||
assert.True(t, ok)
|
||||
assert.False(t, t4.Writeable)
|
||||
t5, ok := findResult(t, result, "toggle5")
|
||||
assert.True(t, ok)
|
||||
assert.False(t, t5.Writeable)
|
||||
t6, ok := findResult(t, result, "toggle6")
|
||||
assert.True(t, ok)
|
||||
assert.False(t, t6.Writeable)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetFeatureToggles(t *testing.T) {
|
||||
t.Run("fails when the user doesn't have write permissions", func(t *testing.T) {
|
||||
s := setting.FeatureMgmtSettings{
|
||||
AllowEditing: true,
|
||||
UpdateWebhook: "random",
|
||||
}
|
||||
features := featuremgmt.WithFeatureManager(s, []*featuremgmt.FeatureFlag{{
|
||||
// Add this here to ensure the feature works as expected during tests
|
||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}})
|
||||
|
||||
b := NewFeatureFlagAPIBuilder(features, actest.FakeAccessControl{ExpectedEvaluate: false}, &setting.Cfg{})
|
||||
msg := callPatchWith(t, b, v0alpha1.ResolvedToggleState{}, http.StatusUnauthorized)
|
||||
assert.Equal(t, "missing write permission", msg)
|
||||
})
|
||||
|
||||
t.Run("fails when update toggle url is not set", func(t *testing.T) {
|
||||
s := setting.FeatureMgmtSettings{
|
||||
AllowEditing: true,
|
||||
}
|
||||
b := newTestAPIBuilder(t, nil, []string{}, s)
|
||||
msg := callPatchWith(t, b, v0alpha1.ResolvedToggleState{}, http.StatusForbidden)
|
||||
assert.Equal(t, "feature toggles are read-only", msg)
|
||||
})
|
||||
|
||||
t.Run("fails with non-existent toggle", func(t *testing.T) {
|
||||
features := []*featuremgmt.FeatureFlag{
|
||||
{
|
||||
Name: "toggle1",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}, {
|
||||
Name: "toggle2",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
},
|
||||
}
|
||||
disabled := []string{"toggle2"}
|
||||
update := v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
"toggle3": true,
|
||||
},
|
||||
}
|
||||
|
||||
s := setting.FeatureMgmtSettings{
|
||||
AllowEditing: true,
|
||||
UpdateWebhook: "random",
|
||||
}
|
||||
b := newTestAPIBuilder(t, features, disabled, s)
|
||||
msg := callPatchWith(t, b, update, http.StatusBadRequest)
|
||||
assert.Equal(t, "invalid toggle passed in", msg)
|
||||
})
|
||||
|
||||
t.Run("fails with read-only toggles", func(t *testing.T) {
|
||||
features := []*featuremgmt.FeatureFlag{
|
||||
{
|
||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}, {
|
||||
Name: "toggle2",
|
||||
Stage: featuremgmt.FeatureStagePublicPreview,
|
||||
}, {
|
||||
Name: "toggle3",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
},
|
||||
}
|
||||
disabled := []string{"toggle2", "toggle3"}
|
||||
|
||||
s := setting.FeatureMgmtSettings{
|
||||
AllowEditing: true,
|
||||
UpdateWebhook: "random",
|
||||
ReadOnlyToggles: map[string]struct{}{
|
||||
"toggle3": {},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("because it is the feature toggle admin page toggle", func(t *testing.T) {
|
||||
update := v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
featuremgmt.FlagFeatureToggleAdminPage: true,
|
||||
},
|
||||
}
|
||||
b := newTestAPIBuilder(t, features, disabled, s)
|
||||
callPatchWith(t, b, update, http.StatusNotModified)
|
||||
})
|
||||
|
||||
t.Run("because it is not GA or Deprecated", func(t *testing.T) {
|
||||
update := v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
"toggle2": true,
|
||||
},
|
||||
}
|
||||
b := newTestAPIBuilder(t, features, disabled, s)
|
||||
msg := callPatchWith(t, b, update, http.StatusBadRequest)
|
||||
assert.Equal(t, "invalid toggle passed in", msg)
|
||||
})
|
||||
|
||||
t.Run("because it is configured to be read-only", func(t *testing.T) {
|
||||
update := v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
"toggle2": true,
|
||||
},
|
||||
}
|
||||
b := newTestAPIBuilder(t, features, disabled, s)
|
||||
msg := callPatchWith(t, b, update, http.StatusBadRequest)
|
||||
assert.Equal(t, "invalid toggle passed in", msg)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when all conditions met", func(t *testing.T) {
|
||||
features := []*featuremgmt.FeatureFlag{
|
||||
{
|
||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}, {
|
||||
Name: "toggle2",
|
||||
Stage: featuremgmt.FeatureStagePublicPreview,
|
||||
}, {
|
||||
Name: "toggle3",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}, {
|
||||
Name: "toggle4",
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
AllowSelfServe: true,
|
||||
}, {
|
||||
Name: "toggle5",
|
||||
Stage: featuremgmt.FeatureStageDeprecated,
|
||||
AllowSelfServe: true,
|
||||
},
|
||||
}
|
||||
disabled := []string{"toggle2", "toggle3", "toggle4"}
|
||||
|
||||
s := setting.FeatureMgmtSettings{
|
||||
AllowEditing: true,
|
||||
UpdateWebhook: "random",
|
||||
UpdateWebhookToken: "token",
|
||||
ReadOnlyToggles: map[string]struct{}{
|
||||
"toggle3": {},
|
||||
},
|
||||
}
|
||||
|
||||
update := v0alpha1.ResolvedToggleState{
|
||||
Enabled: map[string]bool{
|
||||
"toggle4": true,
|
||||
"toggle5": false,
|
||||
},
|
||||
}
|
||||
t.Run("fail when webhook request is not successful", func(t *testing.T) {
|
||||
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}))
|
||||
defer webhookServer.Close()
|
||||
s.UpdateWebhook = webhookServer.URL
|
||||
|
||||
b := newTestAPIBuilder(t, features, disabled, s)
|
||||
msg := callPatchWith(t, b, update, http.StatusInternalServerError)
|
||||
assert.Equal(t, "an error occurred while updating feeature toggles", msg)
|
||||
})
|
||||
|
||||
t.Run("succeed when webhook request is not successful but app is in dev mode", func(t *testing.T) {
|
||||
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}))
|
||||
defer webhookServer.Close()
|
||||
s.UpdateWebhook = webhookServer.URL
|
||||
|
||||
b := newTestAPIBuilder(t, features, disabled, s)
|
||||
b.cfg.Env = setting.Dev
|
||||
callPatchWith(t, b, update, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("succeed when webhook request is successful", func(t *testing.T) {
|
||||
webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "Bearer "+s.UpdateWebhookToken, r.Header.Get("Authorization"))
|
||||
|
||||
var req featuremgmt.FeatureToggleWebhookPayload
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
|
||||
|
||||
assert.Equal(t, "true", req.FeatureToggles["toggle4"])
|
||||
assert.Equal(t, "false", req.FeatureToggles["toggle5"])
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer webhookServer.Close()
|
||||
s.UpdateWebhook = webhookServer.URL
|
||||
|
||||
b := newTestAPIBuilder(t, features, disabled, s)
|
||||
msg := callPatchWith(t, b, update, http.StatusOK)
|
||||
assert.Equal(t, "feature toggles updated successfully", msg)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func findResult(t *testing.T, result v0alpha1.ResolvedToggleState, name string) (v0alpha1.ToggleStatus, bool) {
|
||||
t.Helper()
|
||||
|
||||
for _, t := range result.Toggles {
|
||||
if t.Name == name {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return v0alpha1.ToggleStatus{}, false
|
||||
}
|
||||
|
||||
func callGetWith(t *testing.T, b *FeatureFlagAPIBuilder, expectedCode int) v0alpha1.ResolvedToggleState {
|
||||
w := response.CreateNormalResponse(http.Header{}, []byte{}, 0)
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
Header: http.Header{},
|
||||
}
|
||||
req.Header.Add("content-type", "application/json")
|
||||
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{}))
|
||||
b.handleCurrentStatus(w, req)
|
||||
|
||||
rts := v0alpha1.ResolvedToggleState{}
|
||||
require.NoError(t, json.Unmarshal(w.Body(), &rts))
|
||||
require.Equal(t, expectedCode, w.Status())
|
||||
|
||||
// Tests don't expect the feature toggle admin page feature to be present, so remove them from the resolved toggle state
|
||||
for i, t := range rts.Toggles {
|
||||
if t.Name == "featureToggleAdminPage" {
|
||||
rts.Toggles = append(rts.Toggles[0:i], rts.Toggles[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return rts
|
||||
}
|
||||
|
||||
func callPatchWith(t *testing.T, b *FeatureFlagAPIBuilder, update v0alpha1.ResolvedToggleState, expectedCode int) string {
|
||||
w := response.CreateNormalResponse(http.Header{}, []byte{}, 0)
|
||||
|
||||
body, err := json.Marshal(update)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &http.Request{
|
||||
Method: "PATCH",
|
||||
Body: io.NopCloser(bytes.NewReader(body)),
|
||||
Header: http.Header{},
|
||||
}
|
||||
req.Header.Add("content-type", "application/json")
|
||||
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{}))
|
||||
b.handleCurrentStatus(w, req)
|
||||
|
||||
require.NotNil(t, w.Body())
|
||||
require.Equal(t, expectedCode, w.Status())
|
||||
|
||||
// Extract the public facing message if this is an error
|
||||
if w.Status() > 399 {
|
||||
res := map[string]any{}
|
||||
require.NoError(t, json.Unmarshal(w.Body(), &res))
|
||||
|
||||
return res["message"].(string)
|
||||
}
|
||||
|
||||
return string(w.Body())
|
||||
}
|
||||
|
||||
func newTestAPIBuilder(
|
||||
t *testing.T,
|
||||
serverFeatures []*featuremgmt.FeatureFlag,
|
||||
disabled []string, // the flags that are disabled
|
||||
settings setting.FeatureMgmtSettings,
|
||||
) *FeatureFlagAPIBuilder {
|
||||
t.Helper()
|
||||
features := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{
|
||||
// Add this here to ensure the feature works as expected during tests
|
||||
Name: featuremgmt.FlagFeatureToggleAdminPage,
|
||||
Stage: featuremgmt.FeatureStageGeneralAvailability,
|
||||
}}, serverFeatures...), disabled...)
|
||||
|
||||
return NewFeatureFlagAPIBuilder(features, actest.FakeAccessControl{ExpectedEvaluate: true}, &setting.Cfg{})
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
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 *utils.ResourceInfo
|
||||
tableConverter rest.TableConvertor
|
||||
features *v0alpha1.FeatureList
|
||||
featuresOnce sync.Once
|
||||
}
|
||||
|
||||
// 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() *featuresStorage {
|
||||
resourceInfo := v0alpha1.FeatureResourceInfo
|
||||
return &featuresStorage{
|
||||
resource: &resourceInfo,
|
||||
tableConverter: resourceInfo.TableConverter(),
|
||||
}
|
||||
}
|
||||
|
||||
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) init() {
|
||||
s.featuresOnce.Do(func() {
|
||||
rv := "1"
|
||||
features, _ := featuremgmt.GetEmbeddedFeatureList()
|
||||
for _, feature := range features.Items {
|
||||
if feature.ResourceVersion > rv {
|
||||
rv = feature.ResourceVersion
|
||||
}
|
||||
}
|
||||
features.ResourceVersion = rv
|
||||
s.features = &features
|
||||
})
|
||||
}
|
||||
|
||||
func (s *featuresStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
s.init()
|
||||
if s.features == nil {
|
||||
return nil, fmt.Errorf("error loading embedded features")
|
||||
}
|
||||
return s.features, nil
|
||||
}
|
||||
|
||||
func (s *featuresStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
s.init()
|
||||
|
||||
for idx, flag := range s.features.Items {
|
||||
if flag.Name == name {
|
||||
return &s.features.Items[idx], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"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/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var _ builder.APIGroupBuilder = (*FeatureFlagAPIBuilder)(nil)
|
||||
var _ builder.APIGroupRouteProvider = (*FeatureFlagAPIBuilder)(nil)
|
||||
|
||||
var gv = v0alpha1.SchemeGroupVersion
|
||||
|
||||
// This is used just so wire has something unique to return
|
||||
type FeatureFlagAPIBuilder struct {
|
||||
features *featuremgmt.FeatureManager
|
||||
accessControl accesscontrol.AccessControl
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func NewFeatureFlagAPIBuilder(features *featuremgmt.FeatureManager, accessControl accesscontrol.AccessControl, cfg *setting.Cfg) *FeatureFlagAPIBuilder {
|
||||
return &FeatureFlagAPIBuilder{features, accessControl, cfg}
|
||||
}
|
||||
|
||||
func RegisterAPIService(features *featuremgmt.FeatureManager,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
apiregistration builder.APIRegistrar,
|
||||
cfg *setting.Cfg,
|
||||
registerer prometheus.Registerer,
|
||||
) *FeatureFlagAPIBuilder {
|
||||
builder := NewFeatureFlagAPIBuilder(features, accessControl, cfg)
|
||||
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) AllowedV0Alpha1Resources() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *FeatureFlagAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error {
|
||||
featureStore := NewFeaturesStorage()
|
||||
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 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(gv schema.GroupVersion) *builder.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 &builder.APIRoutes{
|
||||
Root: []builder.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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Handler: b.handleCurrentStatus,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package featuretoggle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
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 *utils.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,7 +6,6 @@ import (
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage"
|
||||
@@ -54,7 +53,6 @@ var WireSet = wire.NewSet(
|
||||
// Each must be added here *and* in the ServiceSink above
|
||||
dashboardinternal.RegisterAPIService,
|
||||
dashboardsnapshot.RegisterAPIService,
|
||||
featuretoggle.RegisterAPIService,
|
||||
datasource.RegisterAPIService,
|
||||
folders.RegisterAPIService,
|
||||
iam.RegisterAPIService,
|
||||
|
||||
@@ -53,7 +53,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/datasource"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/featuretoggle"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage"
|
||||
@@ -811,7 +810,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -863,7 +861,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer)
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer)
|
||||
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1419,7 +1417,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1471,7 +1468,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, featureFlagAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer)
|
||||
apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer)
|
||||
teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user