Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2024-12-16 19:07:54 +03:00
commit e4837d452e
95 files changed
+6686 -5513

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ type PluginSetting struct {
Id string `json:"id"`
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
AutoEnabled bool `json:"autoEnabled"`
Module string `json:"module"`
BaseUrl string `json:"baseUrl"`
Info plugins.Info `json:"info"`
+48 -4
View File
@@ -50,8 +50,6 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
folderPermissionRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
@@ -66,6 +64,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
folderUidRoute.Put("/", handler.updateFolder)
folderUidRoute.Delete("/", handler.deleteFolder)
folderUidRoute.Get("/", handler.getFolder)
folderUidRoute.Get("/counts", handler.countFolderContent)
})
} else {
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
@@ -74,6 +73,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
})
}
})
@@ -747,6 +747,29 @@ func (fk8s *folderK8sHandler) getFolders(c *contextmodel.ReqContext) {
c.JSON(http.StatusOK, hits)
}
func (fk8s *folderK8sHandler) countFolderContent(c *contextmodel.ReqContext) {
client, ok := fk8s.getClient(c)
if !ok {
return
}
uid := web.Params(c.Req)[":uid"]
counts, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}, "counts")
if err != nil {
fk8s.writeError(c, err)
return
}
out, err := toFolderLegacyCounts(counts)
if err != nil {
fk8s.writeError(c, err)
return
}
c.JSON(http.StatusOK, out)
}
func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
client, ok := fk8s.getClient(c)
if !ok {
@@ -988,7 +1011,7 @@ func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f
return nil, nil
}
folderIDs, err := fk8s.getParents(f)
folderIDs, err := getParents(f)
if err != nil {
return nil, err
}
@@ -1004,7 +1027,7 @@ func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f
return metadata, nil
}
func (fk8s *folderK8sHandler) getParents(f *folder.Folder) (map[string]bool, error) {
func getParents(f *folder.Folder) (map[string]bool, error) {
folderIDs := map[string]bool{f.UID: true}
if (f.UID == accesscontrol.GeneralFolderUID) || (f.UID == folder.SharedWithMeFolderUID) {
return folderIDs, nil
@@ -1022,3 +1045,24 @@ func (fk8s *folderK8sHandler) getParents(f *folder.Folder) (map[string]bool, err
return folderIDs, nil
}
func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCounts, error) {
ds, err := folderalpha1.UnstructuredToDescendantCounts(u)
if err != nil {
return nil, err
}
var out = make(folder.DescendantCounts)
for _, v := range ds.Counts {
// if stats come from unified storage, we will use them
if v.Group != "sql-fallback" {
out[v.Resource] = v.Count
continue
}
// if stats are from single tenant DB and they are not in unified storage, we will use them
if _, ok := out[v.Resource]; !ok {
out[v.Resource] = v.Count
}
}
return &out, nil
}
+116
View File
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
clientrest "k8s.io/client-go/rest"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -752,3 +753,118 @@ func TestUpdateFolderLegacyAndUnifiedStorage(t *testing.T) {
}
})
}
func TestToFolderCounts(t *testing.T) {
var tests = []struct {
name string
input *unstructured.Unstructured
expected *folder.DescendantCounts
expectError bool
}{
{
name: "with only counts from unified storage",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": []interface{}{
map[string]interface{}{
"group": "alpha",
"resource": "folders",
"count": int64(1),
},
map[string]interface{}{
"group": "alpha",
"resource": "dashboards",
"count": int64(3),
},
map[string]interface{}{
"group": "alpha",
"resource": "alertRules",
"count": int64(0),
},
},
},
},
expected: &folder.DescendantCounts{
"folders": 1,
"dashboards": 3,
"alertRules": 0,
},
},
{
name: "with counts from both storages",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": []interface{}{
map[string]interface{}{
"group": "alpha",
"resource": "folders",
"count": int64(1),
},
map[string]interface{}{
"group": "alpha",
"resource": "dashboards",
"count": int64(3),
},
map[string]interface{}{
"group": "sql-fallback",
"resource": "folders",
"count": int64(0),
},
},
},
},
expected: &folder.DescendantCounts{
"folders": 1,
"dashboards": 3,
},
},
{
name: "it uses the values from sql-fallaback if not found in unified storage",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": []interface{}{
map[string]interface{}{
"group": "alpha",
"resource": "dashboards",
"count": int64(3),
},
map[string]interface{}{
"group": "sql-fallback",
"resource": "folders",
"count": int64(2),
},
},
},
},
expected: &folder.DescendantCounts{
"folders": 2,
"dashboards": 3,
},
},
{
name: "malformed input",
input: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "folder.grafana.app/v0alpha1",
"counts": map[string]interface{}{},
},
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual, err := toFolderLegacyCounts(tc.input)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
+1
View File
@@ -721,6 +721,7 @@ func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[stri
OrgID: orgID,
Enabled: plugin.AutoEnabled,
Pinned: plugin.AutoEnabled,
AutoEnabled: plugin.AutoEnabled,
PluginVersion: plugin.Info.Version,
}
+6 -1
View File
@@ -216,6 +216,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
if plugin.IsApp() {
dto.Enabled = plugin.AutoEnabled
dto.Pinned = plugin.AutoEnabled
dto.AutoEnabled = plugin.AutoEnabled
}
ps, err := hs.PluginSettings.GetPluginSettingByPluginID(c.Req.Context(), &pluginsettings.GetByPluginIDArgs{
@@ -254,9 +255,13 @@ func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.R
}
pluginID := web.Params(c.Req)[":pluginId"]
if _, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID); !exists {
p, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
}
if p.AutoEnabled && !cmd.Enabled {
return response.Error(http.StatusBadRequest, "Cannot disable auto-enabled plugin", nil)
}
cmd.OrgId = c.SignedInUser.GetOrgID()
cmd.PluginId = pluginID
+40
View File
@@ -871,3 +871,43 @@ func Test_PluginsSettings(t *testing.T) {
})
}
}
func Test_UpdatePluginSetting(t *testing.T) {
pID := "test-app"
p1 := createPlugin(plugins.JSONData{
ID: pID, Type: "app", Name: pID,
Info: plugins.Info{
Version: "1.0.0",
},
AutoEnabled: true,
}, plugins.ClassExternal, plugins.NewFakeFS(),
)
pluginRegistry := &fakes.FakePluginRegistry{
Store: map[string]*plugins.Plugin{
p1.ID: p1,
},
}
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
pID: {ID: 0, OrgID: 1, PluginID: pID, PluginVersion: "1.0.0", Enabled: true},
}}
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.PluginSettings = &pluginSettings
hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{})
hs.pluginFileStore = filestore.ProvideService(pluginRegistry)
hs.managedPluginsService = managedplugins.NewNoop()
hs.log = log.NewNopLogger()
})
input := strings.NewReader(`{"enabled": false}`)
endpoint := fmt.Sprintf("/api/plugins/%s/settings", pID)
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(1, []ac.Permission{{Action: pluginaccesscontrol.ActionWrite, Scope: "plugins:id:test-app"}}))
res, err := server.SendJSON(req)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}