Unistore: Get Folder By ID (#99131)
* Unistore: Get Folder By ID Signed-off-by: Maicon Costa <maiconscosta@gmail.com> --------- Signed-off-by: Maicon Costa <maiconscosta@gmail.com> Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
This commit is contained in:
co-authored by
Stephanie Hingtgen
parent
20d25c6ad9
commit
28ad61ff6c
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user