diff --git a/pkg/api/folder.go b/pkg/api/folder.go index ffbe433e3fd..2c85ef59b77 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -776,7 +776,24 @@ func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) { return // error is already sent } uid := web.Params(c.Req)[":uid"] - out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{}) + + var out *unstructured.Unstructured + var err error + + if uid == accesscontrol.GeneralFolderUID { + out = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "title": folder.RootFolder.Title, + "description": folder.RootFolder.Description, + }, + }, + } + out.SetName(folder.RootFolder.UID) + } else { + out, err = client.Get(c.Req.Context(), uid, v1.GetOptions{}) + } + if err != nil { fk8s.writeError(c, err) return diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 60508e75f3f..decc34ea558 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -863,3 +863,152 @@ func TestToFolderCounts(t *testing.T) { }) } } + +// for now, test only the general folder +func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { + testuser := &user.User{ID: 99, UID: "fdxsqt7t5ryf4a", Login: "testuser"} + + legacyFolder := *folder.RootFolder + + expectedFolder := dtos.Folder{ + UID: legacyFolder.UID, + OrgID: 0, + Title: legacyFolder.Title, + URL: legacyFolder.URL, + HasACL: false, + CanSave: false, + CanEdit: true, + CanAdmin: false, + CanDelete: false, + CreatedBy: "Anonymous", + UpdatedBy: "Anonymous", + } + + mux := http.NewServeMux() + folderApiServerMock := httptest.NewServer(mux) + defer folderApiServerMock.Close() + + t.Run("happy path", func(t *testing.T) { + type testCase struct { + description string + folderUID string + legacyFolder folder.Folder + expectedFolder dtos.Folder + expectedFolderServiceError error + unifiedStorageEnabled bool + unifiedStorageMode grafanarest.DualWriterMode + expectedCode int + } + + tcs := []testCase{ + { + description: "General folder - Legacy", + expectedCode: http.StatusOK, + legacyFolder: legacyFolder, + folderUID: legacyFolder.UID, + expectedFolder: expectedFolder, + unifiedStorageEnabled: false, + }, + { + description: "General folder - Unified storage, mode 1", + expectedCode: http.StatusOK, + legacyFolder: legacyFolder, + folderUID: legacyFolder.UID, + expectedFolder: expectedFolder, + unifiedStorageEnabled: true, + unifiedStorageMode: grafanarest.Mode1, + }, + { + description: "General folder - Unified storage, mode 2", + expectedCode: http.StatusOK, + legacyFolder: legacyFolder, + folderUID: legacyFolder.UID, + expectedFolder: expectedFolder, + unifiedStorageEnabled: true, + unifiedStorageMode: grafanarest.Mode2, + }, + { + description: "General folder - Unified storage, mode 3", + expectedCode: http.StatusOK, + legacyFolder: legacyFolder, + folderUID: legacyFolder.UID, + expectedFolder: expectedFolder, + unifiedStorageEnabled: true, + unifiedStorageMode: grafanarest.Mode3, + }, + { + description: "General folder - Unified storage, mode 4", + expectedCode: http.StatusOK, + legacyFolder: legacyFolder, + folderUID: legacyFolder.UID, + expectedFolder: expectedFolder, + unifiedStorageEnabled: true, + unifiedStorageMode: grafanarest.Mode4, + }, + } + + for _, tc := range tcs { + t.Run(tc.description, func(t *testing.T) { + setUpRBACGuardian(t) + + cfg := setting.NewCfg() + cfg.UnifiedStorage = map[string]setting.UnifiedStorageConfig{ + folderv0alpha1.RESOURCEGROUP: { + DualWriterMode: tc.unifiedStorageMode, + }, + } + + featuresArr := []any{featuremgmt.FlagNestedFolders} + if tc.unifiedStorageEnabled { + featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFolders) + } + + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.folderService = &foldertest.FakeService{ + ExpectedFolder: &tc.legacyFolder, + ExpectedError: tc.expectedFolderServiceError, + } + hs.QuotaService = quotatest.New(false, nil) + hs.SearchService = &mockSearchService{ + ExpectedResult: model.HitList{}, + } + hs.userService = &usertest.FakeUserService{ + ExpectedUser: testuser, + } + hs.Features = featuremgmt.WithFeatures( + featuresArr..., + ) + hs.clientConfigProvider = mockClientConfigProvider{ + host: folderApiServerMock.URL, + } + }) + + req := server.NewRequest(http.MethodGet, fmt.Sprintf("/api/folders/%s", tc.folderUID), nil) + req.Header.Set("Content-Type", "application/json") + webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ + 1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll}, + }), + }}) + + res, err := server.Send(req) + require.NoError(t, err) + + require.Equal(t, tc.expectedCode, res.StatusCode) + defer func() { require.NoError(t, res.Body.Close()) }() + + if tc.expectedCode == http.StatusOK { + body := dtos.Folder{} + require.NoError(t, json.NewDecoder(res.Body).Decode(&body)) + + //nolint:staticcheck + body.ID = 0 + body.Version = 0 + tc.expectedFolder.Version = 0 + require.Equal(t, tc.expectedFolder, body) + } + }) + } + }) +} diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index e76a51801f4..456771c8964 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -92,13 +92,20 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) (*f createdTime = (*created).UTC() } + url := getURL(meta, title) + + // RootFolder does not have URL + if uid == folder.RootFolder.UID { + url = "" + } + f := &folder.Folder{ UID: uid, Title: title, ID: id, ParentUID: meta.GetFolder(), // #TODO add created by field if necessary - URL: getURL(meta, title), + URL: url, // #TODO get Created in format "2024-09-12T15:37:41.09466+02:00" Created: createdTime, // #TODO figure out whether we want to set "updated" and "updated by". Could replace with