fix(graph-metadata): lazy cs3 metadata storage initialization

This commit is contained in:
Florian Schade
2025-05-27 15:32:53 +02:00
parent b37a45f26f
commit f4d8e632fd
19 changed files with 226 additions and 109 deletions

View File

@@ -0,0 +1,166 @@
package metadata
import (
"context"
"errors"
"sync"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-playground/validator/v10"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/opencloud/pkg/storage"
)
// Lazy is a lazy storage implementation that initializes the underlying storage only when needed.
type Lazy struct {
next func() (metadata.Storage, error)
initName string `validate:"required"`
initCTX context.Context `validate:"required"`
}
func NewLazyStorage(next metadata.Storage) (*Lazy, error) {
s := &Lazy{}
s.next = sync.OnceValues[metadata.Storage, error](func() (metadata.Storage, error) {
if err := validator.New(validator.WithPrivateFieldValidation()).Struct(s); err != nil {
return nil, errors.Join(storage.ErrStorageInitialization, storage.ErrStorageValidation, err)
}
if err := next.Init(s.initCTX, s.initName); err != nil {
return nil, errors.Join(storage.ErrStorageInitialization, err)
}
return next, nil
})
return s, nil
}
// Backend wraps the backend of the next storage
func (s *Lazy) Backend() string {
next, err := s.next()
if err != nil {
return ""
}
return next.Backend()
}
// Init prepares the required data for the underlying lazy storage initialization
func (s *Lazy) Init(ctx context.Context, name string) (err error) {
s.initCTX = ctx
s.initName = name
return nil
}
// Upload wraps the upload method of the next storage
func (s *Lazy) Upload(ctx context.Context, req metadata.UploadRequest) (*metadata.UploadResponse, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.Upload(ctx, req)
}
// Download wraps the download method of the next storage
func (s *Lazy) Download(ctx context.Context, req metadata.DownloadRequest) (*metadata.DownloadResponse, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.Download(ctx, req)
}
// SimpleUpload wraps the simple upload method of the next storage
func (s *Lazy) SimpleUpload(ctx context.Context, uploadpath string, content []byte) error {
next, err := s.next()
if err != nil {
return err
}
return next.SimpleUpload(ctx, uploadpath, content)
}
// SimpleDownload wraps the simple download method of the next storage
func (s *Lazy) SimpleDownload(ctx context.Context, path string) ([]byte, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.SimpleDownload(ctx, path)
}
// Delete wraps the delete method of the next storage
func (s *Lazy) Delete(ctx context.Context, path string) error {
next, err := s.next()
if err != nil {
return err
}
return next.Delete(ctx, path)
}
// Stat wraps the stat method of the next storage
func (s *Lazy) Stat(ctx context.Context, path string) (*provider.ResourceInfo, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.Stat(ctx, path)
}
// ReadDir wraps the read directory method of the next storage
func (s *Lazy) ReadDir(ctx context.Context, path string) ([]string, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.ReadDir(ctx, path)
}
// ListDir wraps the list directory method of the next storage
func (s *Lazy) ListDir(ctx context.Context, path string) ([]*provider.ResourceInfo, error) {
next, err := s.next()
if err != nil {
return nil, err
}
return next.ListDir(ctx, path)
}
// CreateSymlink wraps the create symlink method of the next storage
func (s *Lazy) CreateSymlink(ctx context.Context, oldname, newname string) error {
next, err := s.next()
if err != nil {
return err
}
return next.CreateSymlink(ctx, oldname, newname)
}
// ResolveSymlink wraps the resolve symlink method of the next storage
func (s *Lazy) ResolveSymlink(ctx context.Context, name string) (string, error) {
next, err := s.next()
if err != nil {
return "", err
}
return next.ResolveSymlink(ctx, name)
}
// MakeDirIfNotExist wraps the make directory if not exist method of the next storage
func (s *Lazy) MakeDirIfNotExist(ctx context.Context, name string) error {
next, err := s.next()
if err != nil {
return err
}
return next.MakeDirIfNotExist(ctx, name)
}

13
pkg/storage/storage.go Normal file
View File

@@ -0,0 +1,13 @@
package storage
import (
"errors"
)
var (
// ErrStorageInitialization is returned when the storage initialization fails
ErrStorageInitialization = errors.New("failed to initialize storage")
// ErrStorageValidation is returned when the storage configuration is invalid
ErrStorageValidation = errors.New("failed to validate storage configuration")
)

View File

@@ -1,6 +1,8 @@
package http
import (
"context"
"errors"
"fmt"
stdhttp "net/http"
@@ -8,8 +10,7 @@ import (
chimiddleware "github.com/go-chi/chi/v5/middleware"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/pkg/errors"
revaMetadata "github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"go-micro.dev/v4"
"go-micro.dev/v4/events"
@@ -20,6 +21,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/registry"
"github.com/opencloud-eu/opencloud/pkg/service/grpc"
"github.com/opencloud-eu/opencloud/pkg/service/http"
"github.com/opencloud-eu/opencloud/pkg/storage/metadata"
"github.com/opencloud-eu/opencloud/pkg/version"
ehsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/eventhistory/v0"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
@@ -59,7 +61,7 @@ func Server(opts ...Option) (http.Service, error) {
options.Logger.Error().
Err(err).
Msg("Error initializing events publisher")
return http.Service{}, errors.Wrap(err, "could not initialize events publisher")
return http.Service{}, fmt.Errorf("could not initialize events publisher: %w", err)
}
}
@@ -106,7 +108,7 @@ func Server(opts ...Option) (http.Service, error) {
pool.WithTracerProvider(options.TraceProvider),
)...)
if err != nil {
return http.Service{}, errors.Wrap(err, "could not initialize gateway selector")
return http.Service{}, fmt.Errorf("could not initialize gateway selector: %w", err)
}
} else {
middlewares = append(middlewares, graphMiddleware.Token(options.Config.HTTP.APIToken))
@@ -129,21 +131,38 @@ func Server(opts ...Option) (http.Service, error) {
hClient := ehsvc.NewEventHistoryService("eu.opencloud.api.eventhistory", grpcClient)
storage, err := metadata.NewCS3Storage(
options.Config.Metadata.GatewayAddress,
options.Config.Metadata.StorageAddress,
options.Config.Metadata.SystemUserID,
options.Config.Metadata.SystemUserIDP,
options.Config.Metadata.SystemUserAPIKey,
)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize metadata storage: %w", err)
var userProfilePhotoService svc.UsersUserProfilePhotoProvider
{
photoStorage, err := revaMetadata.NewCS3Storage(
options.Config.Metadata.GatewayAddress,
options.Config.Metadata.StorageAddress,
options.Config.Metadata.SystemUserID,
options.Config.Metadata.SystemUserIDP,
options.Config.Metadata.SystemUserAPIKey,
)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize reva metadata storage: %w", err)
}
photoStorage, err = metadata.NewLazyStorage(photoStorage)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize lazy metadata storage: %w", err)
}
if err := photoStorage.Init(context.Background(), "f2bdd61a-da7c-49fc-8203-0558109d1b4f"); err != nil {
return http.Service{}, fmt.Errorf("could not initialize metadata storage: %w", err)
}
userProfilePhotoService, err = svc.NewUsersUserProfilePhotoService(photoStorage)
if err != nil {
return http.Service{}, fmt.Errorf("could not initialize user profile photo service: %w", err)
}
}
var handle svc.Service
handle, err = svc.NewService(
svc.Context(options.Context),
svc.MetadataStorage(storage),
svc.UserProfilePhotoService(userProfilePhotoService),
svc.Logger(options.Logger),
svc.Config(options.Config),
svc.Middleware(middlewares...),
@@ -160,11 +179,11 @@ func Server(opts ...Option) (http.Service, error) {
)
if err != nil {
return http.Service{}, errors.New("could not initialize graph service")
return http.Service{}, fmt.Errorf("could not initialize graph service: %w", err)
}
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
return http.Service{}, err
return http.Service{}, fmt.Errorf("could not register graph service handler: %w", err)
}
return service, nil

View File

@@ -30,9 +30,6 @@ type (
)
var (
// profilePhotoSpaceID is the space ID for the profile photo
profilePhotoSpaceID = "f2bdd61a-da7c-49fc-8203-0558109d1b4f"
// ErrNoBytes is returned when no bytes are found
ErrNoBytes = errors.New("no bytes")
@@ -50,10 +47,6 @@ type UsersUserProfilePhotoService struct {
// NewUsersUserProfilePhotoService creates a new UsersUserProfilePhotoService
func NewUsersUserProfilePhotoService(storage metadata.Storage) (UsersUserProfilePhotoService, error) {
if err := storage.Init(context.Background(), profilePhotoSpaceID); err != nil {
return UsersUserProfilePhotoService{}, err
}
return UsersUserProfilePhotoService{
storage: storage,
}, nil

View File

@@ -19,10 +19,7 @@ import (
)
func TestNewUsersUserProfilePhotoService(t *testing.T) {
storage := mocks.NewStorage(t)
storage.EXPECT().Init(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, id string) error { return nil })
service, err := svc.NewUsersUserProfilePhotoService(storage)
service, err := svc.NewUsersUserProfilePhotoService(mocks.NewStorage(t))
assert.NoError(t, err)
t.Run("UpdatePhoto", func(t *testing.T) {

View File

@@ -71,13 +71,9 @@ var _ = Describe("Applications", func() {
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
cfg.Application.ID = "some-application-ID"
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -81,13 +81,9 @@ var _ = Describe("AppRoleAssignments", func() {
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
cfg.Application.ID = "some-application-ID"
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -85,13 +85,9 @@ var _ = Describe("Driveitems", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -79,13 +79,9 @@ var _ = Describe("EducationClass", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -334,12 +330,8 @@ var _ = Describe("EducationClass", func() {
cfg.API.GroupMembersPatchLimit = 21
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -24,7 +24,6 @@ import (
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
@@ -80,13 +79,9 @@ var _ = Describe("Schools", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.WithIdentityEducationBackend(identityEducationBackend),
)

View File

@@ -81,13 +81,9 @@ var _ = Describe("EducationUsers", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityEducationBackend(identityEducationBackend),

View File

@@ -81,13 +81,9 @@ var _ = Describe("Graph", func() {
eventsPublisher = mocks.Publisher{}
permissionService = mocks.Permissions{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.PermissionService(&permissionService),

View File

@@ -85,13 +85,9 @@ var _ = Describe("Groups", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -417,13 +413,9 @@ var _ = Describe("Groups", func() {
updatedGroupJson, err := json.Marshal(updatedGroup)
Expect(err).ToNot(HaveOccurred())
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
cfg.API.GroupMembersPatchLimit = 21
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -7,7 +7,6 @@ import (
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"go.opentelemetry.io/otel/trace"
"github.com/opencloud-eu/opencloud/pkg/keycloak"
@@ -34,6 +33,7 @@ type Options struct {
IdentityBackend identity.Backend
IdentityEducationBackend identity.EducationBackend
RoleService RoleService
UserProfilePhotoService UsersUserProfilePhotoProvider
PermissionService Permissions
ValueService settingssvc.ValueService
RoleManager *roles.Manager
@@ -43,7 +43,6 @@ type Options struct {
KeycloakClient keycloak.Client
EventHistoryClient ehsvc.EventHistoryService
TraceProvider trace.TracerProvider
Storage metadata.Storage
}
// newOptions initializes the available default options.
@@ -183,9 +182,9 @@ func TraceProvider(val trace.TracerProvider) Option {
}
}
// MetadataStorage provides a function to set the MetadataStorage option.
func MetadataStorage(ms metadata.Storage) Option {
// UserProfilePhotoService provides a function to set the UserProfilePhotoService option.
func UserProfilePhotoService(p UsersUserProfilePhotoProvider) Option {
return func(o *Options) {
o.Storage = ms
o.UserProfilePhotoService = p
}
}

View File

@@ -81,13 +81,9 @@ var _ = Describe("Users changing their own password", func() {
eventsPublisher = mocks.Publisher{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.WithIdentityBackend(identityBackend),
service.EventsPublisher(&eventsPublisher),

View File

@@ -17,6 +17,12 @@ import (
"github.com/jellydator/ttlcache/v3"
microstore "go-micro.dev/v4/store"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
ocldap "github.com/opencloud-eu/opencloud/pkg/ldap"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/registry"
@@ -26,11 +32,6 @@ import (
"github.com/opencloud-eu/opencloud/services/graph/pkg/identity"
graphm "github.com/opencloud-eu/opencloud/services/graph/pkg/middleware"
"github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
)
const (
@@ -170,12 +171,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
return Graph{}, err
}
usersUserProfilePhotoService, err := NewUsersUserProfilePhotoService(options.Storage)
if err != nil {
return Graph{}, err
}
usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(usersUserProfilePhotoService, options.Logger)
usersUserProfilePhotoApi, err := NewUsersUserProfilePhotoApi(options.UserProfilePhotoService, options.Logger)
if err != nil {
return Graph{}, err
}

View File

@@ -246,12 +246,8 @@ var _ = Describe("sharedbyme", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),

View File

@@ -29,7 +29,6 @@ import (
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
@@ -71,13 +70,9 @@ var _ = Describe("SharedWithMe", func() {
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.WithIdentityBackend(identityBackend),
)

View File

@@ -95,13 +95,9 @@ var _ = Describe("Users", func() {
When("OCM is disabled", func() {
BeforeEach(func() {
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -911,12 +907,8 @@ var _ = Describe("Users", func() {
localCfg.API.UsernameMatch = usernameMatch
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
localSvc, err := service.NewService(
service.Config(localCfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
@@ -1137,13 +1129,9 @@ var _ = Describe("Users", func() {
BeforeEach(func() {
cfg.IncludeOCMSharees = true
mds := mocks.NewStorage(GinkgoT())
mds.EXPECT().Init(mock.Anything, mock.Anything).Return(nil)
var err error
svc, err = service.NewService(
service.Config(cfg),
service.MetadataStorage(mds),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),