From 28ad61ff6c956896c563720bce30325cb0c23a92 Mon Sep 17 00:00:00 2001 From: maicon Date: Wed, 22 Jan 2025 01:45:59 -0300 Subject: [PATCH] Unistore: Get Folder By ID (#99131) * Unistore: Get Folder By ID Signed-off-by: Maicon Costa --------- Signed-off-by: Maicon Costa Co-authored-by: Stephanie Hingtgen --- pkg/registry/apis/dashboard/search.go | 4 +- .../dashboards/service/dashboard_service.go | 113 +-------------- .../service/dashboard_service_test.go | 49 ------- .../dashboards/service/search/search.go | 119 ++++++++++++++++ .../dashboards/service/search/search_test.go | 53 +++++++ pkg/services/folder/folderimpl/folder.go | 28 ++-- .../folderimpl/folder_unifiedstorage.go | 85 +++++++++++- .../folderimpl/folder_unifiedstorage_test.go | 131 +++++++++++++++++- .../folder/folderimpl/unifiedstore.go | 7 +- pkg/storage/unified/client.go | 18 +++ .../{ => federatedtests}/stats_test.go | 5 +- 11 files changed, 425 insertions(+), 187 deletions(-) create mode 100644 pkg/services/dashboards/service/search/search.go create mode 100644 pkg/services/dashboards/service/search/search_test.go rename pkg/storage/unified/federated/{ => federatedtests}/stats_test.go (97%) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index f7d33bad1d7..91d9217ed98 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -20,7 +20,7 @@ import ( dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" - dashboardsvc "github.com/grafana/grafana/pkg/services/dashboards/service" + dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/util/errhttp" ) @@ -339,7 +339,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } - parsedResults, err := dashboardsvc.ParseResults(result, searchRequest.Offset) + parsedResults, err := dashboardsearch.ParseResults(result, searchRequest.Offset) if err != nil { errhttp.Write(ctx, err, w) return diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index e6295e0deac..f68ed03495d 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -2,7 +2,6 @@ package service import ( "context" - "encoding/binary" "encoding/json" "errors" "fmt" @@ -21,8 +20,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/selection" - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - claims "github.com/grafana/authlib/types" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" @@ -39,6 +36,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" + dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -71,16 +69,6 @@ var ( tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards/service") ) -var ( - excludedFields = map[string]string{ - resource.SEARCH_FIELD_EXPLAIN: "", - resource.SEARCH_FIELD_SCORE: "", - resource.SEARCH_FIELD_TITLE: "", - resource.SEARCH_FIELD_FOLDER: "", - resource.SEARCH_FIELD_TAGS: "", - } -) - type DashboardServiceImpl struct { cfg *setting.Cfg log log.Logger @@ -1661,7 +1649,7 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex return nil, err } - return ParseResults(res, 0) + return dashboardsearch.ParseResults(res, 0) } type dashboardProvisioningWithUID struct { @@ -1765,103 +1753,6 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, return result, nil } -func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alpha1.SearchResults, error) { - if result == nil { - return nil, nil - } else if result.Error != nil { - return nil, fmt.Errorf("%d error searching: %s: %s", result.Error.Code, result.Error.Message, result.Error.Details) - } else if result.Results == nil { - return nil, nil - } - - titleIDX := 0 - folderIDX := 1 - tagsIDX := -1 - scoreIDX := 0 - explainIDX := 0 - - for i, v := range result.Results.Columns { - switch v.Name { - case resource.SEARCH_FIELD_EXPLAIN: - explainIDX = i - case resource.SEARCH_FIELD_SCORE: - scoreIDX = i - case resource.SEARCH_FIELD_TITLE: - titleIDX = i - case resource.SEARCH_FIELD_FOLDER: - folderIDX = i - case resource.SEARCH_FIELD_TAGS: - tagsIDX = i - } - } - - sr := &v0alpha1.SearchResults{ - Offset: offset, - TotalHits: result.TotalHits, - QueryCost: result.QueryCost, - MaxScore: result.MaxScore, - Hits: make([]v0alpha1.DashboardHit, len(result.Results.Rows)), - } - - for i, row := range result.Results.Rows { - fields := &common.Unstructured{} - for colIndex, col := range result.Results.Columns { - if _, ok := excludedFields[col.Name]; !ok { - val, err := resource.DecodeCell(col, colIndex, row.Cells[colIndex]) - if err != nil { - return nil, err - } - // Some of the dashboard fields come in as int32, but we need to convert them to int64 or else fields.Set() will panic - int32Val, ok := val.(int32) - if ok { - val = int64(int32Val) - } - fields.Set(col.Name, val) - } - } - - hit := &v0alpha1.DashboardHit{ - Resource: row.Key.Resource, // folders | dashboards - Name: row.Key.Name, // The Grafana UID - Title: string(row.Cells[titleIDX]), - Folder: string(row.Cells[folderIDX]), - Field: fields, - } - if tagsIDX > 0 && row.Cells[tagsIDX] != nil { - _ = json.Unmarshal(row.Cells[tagsIDX], &hit.Tags) - } - if explainIDX > 0 && row.Cells[explainIDX] != nil { - _ = json.Unmarshal(row.Cells[explainIDX], &hit.Explain) - } - if scoreIDX > 0 && row.Cells[scoreIDX] != nil { - _, _ = binary.Decode(row.Cells[scoreIDX], binary.BigEndian, &hit.Score) - } - - sr.Hits[i] = *hit - } - - // Add facet results - if result.Facet != nil { - sr.Facets = make(map[string]v0alpha1.FacetResult) - for k, v := range result.Facet { - sr.Facets[k] = v0alpha1.FacetResult{ - Field: v.Field, - Total: v.Total, - Missing: v.Missing, - Terms: make([]v0alpha1.TermFacet, len(v.Terms)), - } - for j, t := range v.Terms { - sr.Facets[k].Terms[j] = v0alpha1.TermFacet{ - Term: t.Term, - Count: t.Count, - } - } - } - } - - return sr, nil -} - func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) { spec, ok := item.Object["spec"].(map[string]any) if !ok { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index eff505300b6..7cdcdf54d69 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/storage/unified/search" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -1818,51 +1817,3 @@ func TestToUID(t *testing.T) { assert.Equal(t, "", result) }) } - -// regression test - parsing int32 values from search results was causing a panic -func TestParseResults(t *testing.T) { - resSearchResp := &resource.ResourceSearchResponse{ - Results: &resource.ResourceTable{ - Columns: []*resource.ResourceTableColumnDefinition{ - { - Name: "title", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: "folder", - Type: resource.ResourceTableColumnDefinition_STRING, - }, - { - Name: search.DASHBOARD_ERRORS_LAST_1_DAYS, - Type: resource.ResourceTableColumnDefinition_INT64, - }, - { - Name: search.DASHBOARD_LINK_COUNT, - Type: resource.ResourceTableColumnDefinition_INT32, - }, - }, - Rows: []*resource.ResourceTableRow{ - { - Key: &resource.ResourceKey{ - Name: "uid", - Resource: "dashboard", - }, - Cells: [][]byte{ - []byte("Dashboard 1"), - []byte("folder1"), - []byte("100"), - []byte("25"), - }, - }, - }, - }, - TotalHits: 1, - } - - res, err := ParseResults(resSearchResp, 0) - - require.NoError(t, err) - hitFields := res.Hits[0].Field.Object - require.Equal(t, int64(100), hitFields[search.DASHBOARD_ERRORS_LAST_1_DAYS]) - require.Equal(t, int64(25), hitFields[search.DASHBOARD_LINK_COUNT]) -} diff --git a/pkg/services/dashboards/service/search/search.go b/pkg/services/dashboards/service/search/search.go new file mode 100644 index 00000000000..bb8a6dddb5e --- /dev/null +++ b/pkg/services/dashboards/service/search/search.go @@ -0,0 +1,119 @@ +package dashboardsearch + +import ( + "encoding/binary" + "encoding/json" + "fmt" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + + "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var ( + excludedFields = map[string]string{ + resource.SEARCH_FIELD_EXPLAIN: "", + resource.SEARCH_FIELD_SCORE: "", + resource.SEARCH_FIELD_TITLE: "", + resource.SEARCH_FIELD_FOLDER: "", + resource.SEARCH_FIELD_TAGS: "", + } +) + +func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alpha1.SearchResults, error) { + if result == nil { + return nil, nil + } else if result.Error != nil { + return nil, fmt.Errorf("%d error searching: %s: %s", result.Error.Code, result.Error.Message, result.Error.Details) + } else if result.Results == nil { + return nil, nil + } + + titleIDX := 0 + folderIDX := 1 + tagsIDX := -1 + scoreIDX := 0 + explainIDX := 0 + + for i, v := range result.Results.Columns { + switch v.Name { + case resource.SEARCH_FIELD_EXPLAIN: + explainIDX = i + case resource.SEARCH_FIELD_SCORE: + scoreIDX = i + case resource.SEARCH_FIELD_TITLE: + titleIDX = i + case resource.SEARCH_FIELD_FOLDER: + folderIDX = i + case resource.SEARCH_FIELD_TAGS: + tagsIDX = i + } + } + + sr := &v0alpha1.SearchResults{ + Offset: offset, + TotalHits: result.TotalHits, + QueryCost: result.QueryCost, + MaxScore: result.MaxScore, + Hits: make([]v0alpha1.DashboardHit, len(result.Results.Rows)), + } + + for i, row := range result.Results.Rows { + fields := &common.Unstructured{} + for colIndex, col := range result.Results.Columns { + if _, ok := excludedFields[col.Name]; !ok { + val, err := resource.DecodeCell(col, colIndex, row.Cells[colIndex]) + if err != nil { + return nil, err + } + // Some of the dashboard fields come in as int32, but we need to convert them to int64 or else fields.Set() will panic + int32Val, ok := val.(int32) + if ok { + val = int64(int32Val) + } + fields.Set(col.Name, val) + } + } + + hit := &v0alpha1.DashboardHit{ + Resource: row.Key.Resource, // folders | dashboards + Name: row.Key.Name, // The Grafana UID + Title: string(row.Cells[titleIDX]), + Folder: string(row.Cells[folderIDX]), + Field: fields, + } + if tagsIDX > 0 && row.Cells[tagsIDX] != nil { + _ = json.Unmarshal(row.Cells[tagsIDX], &hit.Tags) + } + if explainIDX > 0 && row.Cells[explainIDX] != nil { + _ = json.Unmarshal(row.Cells[explainIDX], &hit.Explain) + } + if scoreIDX > 0 && row.Cells[scoreIDX] != nil { + _, _ = binary.Decode(row.Cells[scoreIDX], binary.BigEndian, &hit.Score) + } + + sr.Hits[i] = *hit + } + + // Add facet results + if result.Facet != nil { + sr.Facets = make(map[string]v0alpha1.FacetResult) + for k, v := range result.Facet { + sr.Facets[k] = v0alpha1.FacetResult{ + Field: v.Field, + Total: v.Total, + Missing: v.Missing, + Terms: make([]v0alpha1.TermFacet, len(v.Terms)), + } + for j, t := range v.Terms { + sr.Facets[k].Terms[j] = v0alpha1.TermFacet{ + Term: t.Term, + Count: t.Count, + } + } + } + } + + return sr, nil +} diff --git a/pkg/services/dashboards/service/search/search_test.go b/pkg/services/dashboards/service/search/search_test.go new file mode 100644 index 00000000000..77107378737 --- /dev/null +++ b/pkg/services/dashboards/service/search/search_test.go @@ -0,0 +1,53 @@ +package dashboardsearch + +import ( + "testing" + + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/search" + "github.com/stretchr/testify/require" +) + +// regression test - parsing int32 values from search results was causing a panic +func TestParseResults(t *testing.T) { + resSearchResp := &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: search.DASHBOARD_ERRORS_LAST_1_DAYS, + Type: resource.ResourceTableColumnDefinition_INT64, + }, + { + Name: search.DASHBOARD_LINK_COUNT, + Type: resource.ResourceTableColumnDefinition_INT32, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder1"), + []byte("100"), + []byte("25"), + }, + }, + }, + }, + TotalHits: 1, + } + + _, err := ParseResults(resSearchResp, 0) + require.NoError(t, err) +} diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index e05b6f896aa..cdcb9c2ee2b 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -38,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified" "github.com/grafana/grafana/pkg/util" ) @@ -76,21 +77,11 @@ func ProvideService( r prometheus.Registerer, tracer tracing.Tracer, ) *Service { - k8sHandler := &foldk8sHandler{ - gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(), - namespacer: request.GetNamespaceMapper(cfg), - cfg: cfg, - restConfigProvider: apiserver.GetRestConfig, - } - - unifiedStore := ProvideUnifiedStore(k8sHandler) - srv := &Service{ log: slog.Default().With("logger", "folder-service"), dashboardStore: dashboardStore, dashboardFolderStore: folderStore, store: store, - unifiedStore: unifiedStore, features: features, accessControl: ac, bus: bus, @@ -98,7 +89,6 @@ func ProvideService( registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(r), tracer: tracer, - k8sclient: k8sHandler, } srv.DBMigration(db) @@ -106,6 +96,22 @@ func ProvideService( ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv)) ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv)) + + if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) { + k8sHandler := &foldk8sHandler{ + gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(), + namespacer: request.GetNamespaceMapper(cfg), + cfg: cfg, + restConfigProvider: apiserver.GetRestConfig, + recourceClientProvider: unified.GetResourceClient, + } + + unifiedStore := ProvideUnifiedStore(k8sHandler) + + srv.unifiedStore = unifiedStore + srv.k8sclient = k8sHandler + } + return srv } diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 77a94d42688..d9473e3659b 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -2,6 +2,7 @@ package folderimpl import ( "context" + "fmt" "strings" "time" @@ -9,21 +10,26 @@ import ( "go.opentelemetry.io/otel/trace" "golang.org/x/exp/slices" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/selection" "k8s.io/client-go/dynamic" clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" + dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/util" ) @@ -31,15 +37,17 @@ import ( type folderK8sHandler interface { getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) getNamespace(orgID int64) string + getSearcher(ctx context.Context) resource.ResourceClient } var _ folderK8sHandler = (*foldk8sHandler)(nil) type foldk8sHandler struct { - cfg *setting.Cfg - namespacer request.NamespaceMapper - gvr schema.GroupVersionResource - restConfigProvider func(ctx context.Context) *clientrest.Config + cfg *setting.Cfg + namespacer request.NamespaceMapper + gvr schema.GroupVersionResource + restConfigProvider func(ctx context.Context) *clientrest.Config + recourceClientProvider func(ctx context.Context) resource.ResourceClient } func (s *Service) getFoldersFromApiServer(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) { @@ -93,6 +101,8 @@ func (s *Service) getFromApiServer(ctx context.Context, q *folder.GetFolderQuery return folder.SharedWithMeFolder.WithURL(), nil } + ctx = identity.WithRequester(ctx, q.SignedInUser) + var dashFolder *folder.Folder var err error switch { @@ -102,13 +112,17 @@ func (s *Service) getFromApiServer(ctx context.Context, q *folder.GetFolderQuery } dashFolder, err = s.unifiedStore.Get(ctx, *q) if err != nil { - return nil, err + return nil, toFolderError(err) } // nolint:staticcheck case q.ID != nil: - // not implemented + dashFolder, err = s.getFolderByIDFromApiServer(ctx, *q.ID, q.OrgID) + if err != nil { + return nil, toFolderError(err) + } case q.Title != nil: // not implemented + return nil, folder.ErrBadRequest.Errorf("not implemented") default: return nil, folder.ErrBadRequest.Errorf("either on of UID, ID, Title fields must be present") } @@ -155,6 +169,61 @@ func (s *Service) getFromApiServer(ctx context.Context, q *folder.GetFolderQuery return f, err } +func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgID int64) (*folder.Folder, error) { + if id == 0 { + return &folder.GeneralFolder, nil + } + + folderkey := &resource.ResourceKey{ + Namespace: s.k8sclient.getNamespace(orgID), + Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, + Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + } + + request := &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: folderkey, + Fields: []*resource.Requirement{}, + Labels: []*resource.Requirement{ + { + Key: utils.LabelKeyDeprecatedInternalID, // nolint:staticcheck + Operator: string(selection.In), + Values: []string{fmt.Sprintf("%d", id)}, + }, + }, + }, + Limit: 100000} + + client := s.k8sclient.getSearcher(ctx) + + res, err := client.Search(ctx, request) + if err != nil { + return nil, err + } + + hits, err := dashboardsearch.ParseResults(res, 0) + if err != nil { + return nil, err + } + + if len(hits.Hits) == 0 { + return nil, dashboards.ErrFolderNotFound + } + + uid := hits.Hits[0].Name + user, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + f, err := s.Get(ctx, &folder.GetFolderQuery{UID: &uid, SignedInUser: user, OrgID: orgID}) + if err != nil { + return nil, err + } + + return f, nil +} + func (s *Service) getChildrenFromApiServer(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) { defer func(t time.Time) { parent := q.UID @@ -697,3 +766,7 @@ func (fk8s *foldk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic func (fk8s *foldk8sHandler) getNamespace(orgID int64) string { return fk8s.namespacer(orgID) } + +func (fk8s *foldk8sHandler) getSearcher(ctx context.Context) resource.ResourceClient { + return fk8s.recourceClientProvider(ctx) +} diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index fa95434170f..ff445b16832 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -11,9 +11,11 @@ import ( "testing" "github.com/stretchr/testify/require" + "google.golang.org/grpc" clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" @@ -31,6 +33,7 @@ import ( ngstore "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/storage/unified/resource" ) type rcp struct { @@ -54,6 +57,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { unifiedStorageFolder.Kind = "folder" fooFolder := &folder.Folder{ + ID: 123, Title: "Foo Folder", OrgID: orgID, UID: "foo", @@ -163,11 +167,16 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { Host: folderApiServerMock.URL, } + f := func(ctx context.Context) resource.ResourceClient { + return resourceClientMock{} + } + k8sHandler := &foldk8sHandler{ - gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(), - namespacer: request.GetNamespaceMapper(cfg), - cfg: cfg, - restConfigProvider: restCfgProvider.GetRestConfig, + gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(), + namespacer: request.GetNamespaceMapper(cfg), + cfg: cfg, + restConfigProvider: restCfgProvider.GetRestConfig, + recourceClientProvider: f, } unifiedStore := ProvideUnifiedStore(k8sHandler) @@ -205,6 +214,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { registry: make(map[string]folder.RegistryService), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), + k8sclient: k8sHandler, } require.NoError(t, folderService.RegisterService(alertingStore)) @@ -402,6 +412,32 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { require.NoError(t, err) }) + t.Run("When get folder by ID should return folder", func(t *testing.T) { + id := int64(123) + query := &folder.GetFolderQuery{ + ID: &id, + OrgID: 1, + SignedInUser: usr, + } + + actual, err := folderService.Get(context.Background(), query) + require.Equal(t, fooFolder, actual) + require.NoError(t, err) + }) + + t.Run("When get folder by non existing ID should return not found error", func(t *testing.T) { + id := int64(111111) + query := &folder.GetFolderQuery{ + ID: &id, + OrgID: 1, + SignedInUser: usr, + } + + actual, err := folderService.Get(context.Background(), query) + require.Nil(t, actual) + require.ErrorIs(t, err, dashboards.ErrFolderNotFound) + }) + // TODO!! /* t.Run("When get folder by title should return folder", func(t *testing.T) { @@ -434,3 +470,90 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { }) }) } + +type resourceClientMock struct{} + +func (r resourceClientMock) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) { + return nil, nil +} +func (r resourceClientMock) Create(ctx context.Context, in *resource.CreateRequest, opts ...grpc.CallOption) (*resource.CreateResponse, error) { + return nil, nil +} +func (r resourceClientMock) Update(ctx context.Context, in *resource.UpdateRequest, opts ...grpc.CallOption) (*resource.UpdateResponse, error) { + return nil, nil +} +func (r resourceClientMock) Delete(ctx context.Context, in *resource.DeleteRequest, opts ...grpc.CallOption) (*resource.DeleteResponse, error) { + return nil, nil +} +func (r resourceClientMock) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) { + return nil, nil +} +func (r resourceClientMock) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) { + return nil, nil +} +func (r resourceClientMock) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) { + return nil, nil +} +func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { + if len(in.Options.Labels) > 0 && + in.Options.Labels[0].Key == utils.LabelKeyDeprecatedInternalID && + in.Options.Labels[0].Operator == "in" && + len(in.Options.Labels[0].Values) > 0 && + in.Options.Labels[0].Values[0] == "123" { + return &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "_id", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "title", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + { + Name: "folder", + Type: resource.ResourceTableColumnDefinition_STRING, + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "foo", + Resource: "folders", + }, + Cells: [][]byte{ + []byte("123"), + []byte("folder1"), + []byte(""), + }, + }, + }, + }, + TotalHits: 1, + }, nil + } + + // not found + return &resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{}, + }, nil +} +func (r resourceClientMock) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) { + return nil, nil +} +func (r resourceClientMock) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) { + return nil, nil +} +func (r resourceClientMock) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) { + return nil, nil +} +func (r resourceClientMock) PutBlob(ctx context.Context, in *resource.PutBlobRequest, opts ...grpc.CallOption) (*resource.PutBlobResponse, error) { + return nil, nil +} +func (r resourceClientMock) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) { + return nil, nil +} +func (r resourceClientMock) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) { + return nil, nil +} diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index b3ebe609d47..05f9819662b 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -7,6 +7,7 @@ import ( "time" "github.com/grafana/grafana/pkg/apimachinery/identity" + apierrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8sUser "k8s.io/apiserver/pkg/authentication/user" @@ -15,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/util" ) @@ -157,10 +159,11 @@ func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQue } out, err := client.Get(newCtx, *q.UID, v1.GetOptions{}) - if err != nil { + if err != nil && !apierrors.IsNotFound(err) { return nil, err + } else if err != nil || out == nil { + return nil, dashboards.ErrFolderNotFound } - dashFolder, _ := internalfolders.UnstructuredToLegacyFolder(*out, q.SignedInUser.GetOrgID()) return dashFolder, nil } diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 05cdbe9caf1..cd413c3a5d2 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -28,6 +28,17 @@ import ( const resourceStoreAudience = "resourceStore" +var ( + // internal provider of the package level resource client + pkgResourceClient resource.ResourceClient + ready = make(chan struct{}) +) + +func GetResourceClient(ctx context.Context) resource.ResourceClient { + <-ready + return pkgResourceClient +} + // This adds a UnifiedStorage client into the wire dependency tree func ProvideUnifiedStorageClient( cfg *setting.Cfg, @@ -53,6 +64,13 @@ func ProvideUnifiedStorageClient( legacysql.NewDatabaseProvider(db), ) } + + // only set the package level restConfig once + if pkgResourceClient == nil { + pkgResourceClient = client + close(ready) + } + return client, err } diff --git a/pkg/storage/unified/federated/stats_test.go b/pkg/storage/unified/federated/federatedtests/stats_test.go similarity index 97% rename from pkg/storage/unified/federated/stats_test.go rename to pkg/storage/unified/federated/federatedtests/stats_test.go index cc24573fae0..c0256cf99dc 100644 --- a/pkg/storage/unified/federated/stats_test.go +++ b/pkg/storage/unified/federated/federatedtests/stats_test.go @@ -1,4 +1,4 @@ -package federated +package federatedtests import ( "context" @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/storage/legacysql" + "github.com/grafana/grafana/pkg/storage/unified/federated" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/tests/testsuite" ) @@ -102,7 +103,7 @@ func TestDirectSQLStats(t *testing.T) { }) require.NoError(t, err) - store := &LegacyStatsGetter{ + store := &federated.LegacyStatsGetter{ SQL: legacysql.NewDatabaseProvider(db), }