chore: bump reva
This commit is contained in:
committed by
Ralf Haferkamp
parent
393926bd73
commit
59bd11d02a
Generated
Vendored
-10
@@ -744,16 +744,6 @@ func (s *Service) Delete(ctx context.Context, req *provider.DeleteRequest) (*pro
|
||||
}
|
||||
|
||||
ctx = ctxpkg.ContextSetLockID(ctx, req.LockId)
|
||||
|
||||
// check DeleteRequest for any known opaque properties.
|
||||
// FIXME these should be part of the DeleteRequest object
|
||||
if req.Opaque != nil {
|
||||
if _, ok := req.Opaque.Map["deleting_shared_resource"]; ok {
|
||||
// it is a binary key; its existence signals true. Although, do not assume.
|
||||
ctx = appctx.WithDeletingSharedResource(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
md, err := s.Storage.GetMD(ctx, req.Ref, []string{}, []string{"id", "status"})
|
||||
if err != nil {
|
||||
return &provider.DeleteResponse{
|
||||
|
||||
Generated
Vendored
+10
-4
@@ -182,9 +182,12 @@ func (s *service) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*use
|
||||
user, err := s.usermgr.GetUser(ctx, req.UserId, req.SkipFetchingUserGroups)
|
||||
if err != nil {
|
||||
res := &userpb.GetUserResponse{}
|
||||
if _, ok := err.(errtypes.NotFound); ok {
|
||||
switch err.(type) {
|
||||
case errtypes.NotFound:
|
||||
res.Status = status.NewNotFound(ctx, "user not found")
|
||||
} else {
|
||||
case errtypes.Unavailable:
|
||||
res.Status = status.NewUnavailable(ctx, "user provider temporarily unavailable")
|
||||
default:
|
||||
res.Status = status.NewInternal(ctx, "error getting user")
|
||||
}
|
||||
return res, nil
|
||||
@@ -205,9 +208,12 @@ func (s *service) GetUserByClaim(ctx context.Context, req *userpb.GetUserByClaim
|
||||
user, err := s.usermgr.GetUserByClaim(ctx, req.Claim, req.Value, tenantID, req.SkipFetchingUserGroups)
|
||||
if err != nil {
|
||||
res := &userpb.GetUserByClaimResponse{}
|
||||
if _, ok := err.(errtypes.NotFound); ok {
|
||||
switch err.(type) {
|
||||
case errtypes.NotFound:
|
||||
res.Status = status.NewNotFound(ctx, fmt.Sprintf("user not found %s %s", req.Claim, req.Value))
|
||||
} else {
|
||||
case errtypes.Unavailable:
|
||||
res.Status = status.NewUnavailable(ctx, "user provider temporarily unavailable")
|
||||
default:
|
||||
res.Status = status.NewInternal(ctx, "error getting user by claim")
|
||||
}
|
||||
return res, nil
|
||||
|
||||
vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider/usershareprovider.go
Generated
Vendored
+4
-4
@@ -84,9 +84,9 @@ type service struct {
|
||||
allowedPathsForShares []*regexp.Regexp
|
||||
}
|
||||
|
||||
func getShareManager(c *config) (share.Manager, error) {
|
||||
func getShareManager(c *config, logger *zerolog.Logger) (share.Manager, error) {
|
||||
if f, ok := registry.NewFuncs[c.Driver]; ok {
|
||||
return f(c.Drivers[c.Driver])
|
||||
return f(c.Drivers[c.Driver], logger)
|
||||
}
|
||||
return nil, errtypes.NotFound("driver not found: " + c.Driver)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New creates a new user share provider svc initialized from defaults
|
||||
func NewDefault(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
|
||||
func NewDefault(m map[string]any, ss *grpc.Server, logger *zerolog.Logger) (rgrpc.Service, error) {
|
||||
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
@@ -123,7 +123,7 @@ func NewDefault(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (r
|
||||
|
||||
c.init()
|
||||
|
||||
sm, err := getShareManager(c)
|
||||
sm, err := getShareManager(c, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Generated
Vendored
+7
@@ -155,6 +155,13 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
}
|
||||
for j := range patches[i].Props {
|
||||
propNameXML := patches[i].Props[j].XMLName
|
||||
|
||||
// favorites are now managed by the Graph API and can no longer be set using PROPPATCH. To avoid confusion, we return a 403 Forbidden when clients try to set the oc:favorites property
|
||||
if propNameXML.Local == "favorite" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// don't use path.Join. It removes the double slash! concatenate with a /
|
||||
key := fmt.Sprintf("%s/%s", patches[i].Props[j].XMLName.Space, patches[i].Props[j].XMLName.Local)
|
||||
value := string(patches[i].Props[j].InnerXML)
|
||||
|
||||
-10
@@ -27,16 +27,6 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// deletingSharedResource flags to a storage a shared resource is being deleted not by the owner.
|
||||
type deletingSharedResource struct{}
|
||||
|
||||
func WithDeletingSharedResource(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, deletingSharedResource{}, struct{}{})
|
||||
}
|
||||
func DeletingSharedResourceFromContext(ctx context.Context) bool {
|
||||
return ctx.Value(deletingSharedResource{}) != nil
|
||||
}
|
||||
|
||||
// WithLogger returns a context with an associated logger.
|
||||
func WithLogger(ctx context.Context, l *zerolog.Logger) context.Context {
|
||||
return l.WithContext(ctx)
|
||||
|
||||
+21
@@ -203,6 +203,15 @@ func (e TooEarly) Error() string { return "error: too early: " + string(e) }
|
||||
// IsTooEarly implements the IsTooEarly interface.
|
||||
func (e TooEarly) IsTooEarly() {}
|
||||
|
||||
// Unavailable is the error to use when a backend service (e.g. LDAP, database) is
|
||||
// temporarily unreachable. Callers should treat this as a transient failure and retry.
|
||||
type Unavailable string
|
||||
|
||||
func (e Unavailable) Error() string { return "error: unavailable: " + string(e) }
|
||||
|
||||
// IsUnavailable implements the IsUnavailable interface.
|
||||
func (e Unavailable) IsUnavailable() {}
|
||||
|
||||
// IsNotFound is the interface to implement
|
||||
// to specify that a resource is not found.
|
||||
type IsNotFound interface {
|
||||
@@ -293,6 +302,12 @@ type IsTooEarly interface {
|
||||
IsTooEarly()
|
||||
}
|
||||
|
||||
// IsUnavailable is the interface to implement to specify that a backend service is
|
||||
// temporarily unavailable and the caller should retry.
|
||||
type IsUnavailable interface {
|
||||
IsUnavailable()
|
||||
}
|
||||
|
||||
// NewErrtypeFromStatus maps a rpc status to an errtype
|
||||
func NewErrtypeFromStatus(status *rpc.Status) error {
|
||||
switch status.Code {
|
||||
@@ -329,6 +344,8 @@ func NewErrtypeFromStatus(status *rpc.Status) error {
|
||||
return BadRequest(status.Message)
|
||||
case rpc.Code_CODE_TOO_EARLY:
|
||||
return TooEarly(status.Message)
|
||||
case rpc.Code_CODE_UNAVAILABLE:
|
||||
return Unavailable(status.Message)
|
||||
default:
|
||||
return InternalError(status.Message)
|
||||
}
|
||||
@@ -363,6 +380,8 @@ func NewErrtypeFromHTTPStatusCode(code int, message string) error {
|
||||
return PartialContent(message)
|
||||
case http.StatusTooEarly:
|
||||
return TooEarly(message)
|
||||
case http.StatusServiceUnavailable:
|
||||
return Unavailable(message)
|
||||
case StatusChecksumMismatch:
|
||||
return ChecksumMismatch(message)
|
||||
default:
|
||||
@@ -399,6 +418,8 @@ func NewHTTPStatusCodeFromErrtype(err error) int {
|
||||
return http.StatusPartialContent
|
||||
case TooEarly:
|
||||
return http.StatusTooEarly
|
||||
case Unavailable:
|
||||
return http.StatusServiceUnavailable
|
||||
case ChecksumMismatch:
|
||||
return StatusChecksumMismatch
|
||||
default:
|
||||
|
||||
+25
-21
@@ -71,9 +71,8 @@ type RawStream struct {
|
||||
c Config
|
||||
}
|
||||
|
||||
func FromConfig(ctx context.Context, name string, cfg Config) (Stream, error) {
|
||||
var s Stream
|
||||
b := backoff.NewExponentialBackOff()
|
||||
func JetStream(ctx context.Context, name string, cfg Config) (jetstream.JetStream, error) {
|
||||
var js jetstream.JetStream
|
||||
|
||||
connect := func() error {
|
||||
var tlsConf *tls.Config
|
||||
@@ -120,27 +119,32 @@ func FromConfig(ctx context.Context, name string, cfg Config) (Stream, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
jsConn, err := jetstream.New(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
js, err := jsConn.Stream(ctx, events.MainQueueName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s = &RawStream{
|
||||
js: js,
|
||||
c: cfg,
|
||||
}
|
||||
return nil
|
||||
js, err = jetstream.New(conn)
|
||||
return err
|
||||
}
|
||||
err := backoff.Retry(connect, b)
|
||||
|
||||
err := backoff.Retry(connect, backoff.NewExponentialBackOff())
|
||||
if err != nil {
|
||||
return s, errors.Wrap(err, "could not connect to nats jetstream")
|
||||
return nil, errors.Wrap(err, "could not connect to nats jetstream")
|
||||
}
|
||||
return s, nil
|
||||
return js, nil
|
||||
}
|
||||
|
||||
func FromConfig(ctx context.Context, name string, cfg Config) (Stream, error) {
|
||||
jsConn, err := JetStream(ctx, name, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
js, err := jsConn.Stream(ctx, events.MainQueueName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &RawStream{
|
||||
js: js,
|
||||
c: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *RawStream) Consume(group string, evs ...events.Unmarshaller) (<-chan Event, error) {
|
||||
|
||||
+35
-1
@@ -2,6 +2,7 @@ package stream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
@@ -11,7 +12,9 @@ import (
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/go-micro/plugins/v4/events/natsjs"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events/raw"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -65,7 +68,38 @@ func NatsFromConfig(connName string, disableDurability bool, cfg NatsConfig) (ev
|
||||
opts = append(opts, natsjs.DisableDurableStreams())
|
||||
}
|
||||
|
||||
return Nats(opts...)
|
||||
s, err := Nats(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// apply a MaxAge to the main queue to prevent it from filling up
|
||||
ctx := context.Background()
|
||||
jsConn, err := raw.JetStream(ctx, connName, raw.Config{
|
||||
Endpoint: cfg.Endpoint,
|
||||
Cluster: cfg.Cluster,
|
||||
TLSInsecure: cfg.TLSInsecure,
|
||||
TLSRootCACertificate: cfg.TLSRootCACertificate,
|
||||
EnableTLS: cfg.EnableTLS,
|
||||
AuthUsername: cfg.AuthUsername,
|
||||
AuthPassword: cfg.AuthPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
streamCfg := jetstream.StreamConfig{
|
||||
Name: "main-queue",
|
||||
MaxAge: 7 * 24 * time.Hour,
|
||||
}
|
||||
_, err = jsConn.CreateStream(ctx, streamCfg)
|
||||
if err != nil {
|
||||
// If the stream already exists, update its configuration
|
||||
if err == jetstream.ErrStreamNameAlreadyInUse {
|
||||
_, _ = jsConn.UpdateStream(ctx, streamCfg)
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// nats returns a nats streaming client
|
||||
|
||||
+13
@@ -68,6 +68,15 @@ func NewInternal(ctx context.Context, msg string) *rpc.Status {
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnavailable returns a Status with CODE_UNAVAILABLE.
|
||||
func NewUnavailable(ctx context.Context, msg string) *rpc.Status {
|
||||
return &rpc.Status{
|
||||
Code: rpc.Code_CODE_UNAVAILABLE,
|
||||
Message: msg,
|
||||
Trace: getTrace(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnauthenticated returns a Status with CODE_UNAUTHENTICATED.
|
||||
func NewUnauthenticated(ctx context.Context, err error, msg string) *rpc.Status {
|
||||
return &rpc.Status{
|
||||
@@ -191,6 +200,10 @@ func NewStatusFromErrType(ctx context.Context, msg string, err error) *rpc.Statu
|
||||
return NewUnimplemented(ctx, err, msg+":"+err.Error())
|
||||
case errtypes.BadRequest:
|
||||
return NewInvalid(ctx, msg+":"+err.Error())
|
||||
case errtypes.Unavailable:
|
||||
return NewUnavailable(ctx, msg+": "+err.Error())
|
||||
case errtypes.IsUnavailable:
|
||||
return NewUnavailable(ctx, msg+": "+err.Error())
|
||||
}
|
||||
|
||||
// map GRPC status codes coming from the auth middleware
|
||||
|
||||
+170
-60
@@ -36,9 +36,9 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/logger"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
migration "github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/migrations"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/providercache"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/receivedsharecache"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/sharecache"
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
@@ -122,14 +123,20 @@ var (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
MaxConcurrency int `mapstructure:"max_concurrency"`
|
||||
ProviderAddr string `mapstructure:"provider_addr"`
|
||||
ServiceUserID string `mapstructure:"service_user_id"`
|
||||
ServiceUserIdp string `mapstructure:"service_user_idp"`
|
||||
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
|
||||
CacheTTL int `mapstructure:"ttl"`
|
||||
Events EventOptions `mapstructure:"events"`
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
MaxConcurrency int `mapstructure:"max_concurrency"`
|
||||
ProviderAddr string `mapstructure:"provider_addr"`
|
||||
SystemUserID string `mapstructure:"system_user_id"`
|
||||
SystemUserIdp string `mapstructure:"system_user_idp"`
|
||||
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
|
||||
ServiceAccountID string `mapstructure:"service_account_id"`
|
||||
ServiceAccountSecret string `mapstructure:"service_account_secret"`
|
||||
// ProviderRegistryAddr is the address of the storage registry used during
|
||||
// migrations. Defaults to GatewayAddr when empty, because in the default
|
||||
// OpenCloud deployment the registry is co-located with the gateway.
|
||||
ProviderRegistryAddr string `mapstructure:"provider_registry_addr"`
|
||||
CacheTTL int `mapstructure:"ttl"`
|
||||
Events EventOptions `mapstructure:"events"`
|
||||
}
|
||||
|
||||
// EventOptions are the configurable options for events
|
||||
@@ -145,8 +152,6 @@ type EventOptions struct {
|
||||
|
||||
// Manager implements a share manager using a cs3 storage backend with local caching
|
||||
type Manager struct {
|
||||
sync.RWMutex
|
||||
|
||||
Cache providercache.Cache // holds all shares, sharded by provider id and space id
|
||||
CreatedCache sharecache.Cache // holds the list of shares a user has created, sharded by user id
|
||||
GroupReceivedCache sharecache.Cache // holds the list of shares a group has access to, sharded by group id
|
||||
@@ -155,23 +160,25 @@ type Manager struct {
|
||||
storage metadata.Storage
|
||||
SpaceRoot *provider.ResourceId
|
||||
|
||||
initialized bool
|
||||
ready chan struct{} // closed once initialize() has completed successfully
|
||||
migrationsDone chan struct{} // closed once doMigrations() has returned on this instance
|
||||
|
||||
MaxConcurrency int
|
||||
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
|
||||
eventStream events.Stream
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
// NewDefault returns a new manager instance with default dependencies
|
||||
func NewDefault(m map[string]interface{}) (share.Manager, error) {
|
||||
func NewDefault(m map[string]interface{}, logger *zerolog.Logger) (share.Manager, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := metadata.NewCS3Storage(c.ProviderAddr, c.ProviderAddr, c.ServiceUserID, c.ServiceUserIdp, c.MachineAuthAPIKey)
|
||||
s, err := metadata.NewCS3Storage(c.ProviderAddr, c.ProviderAddr, c.SystemUserID, c.SystemUserIdp, c.MachineAuthAPIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -189,11 +196,34 @@ func NewDefault(m map[string]interface{}) (share.Manager, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return New(s, gatewaySelector, c.CacheTTL, es, c.MaxConcurrency)
|
||||
mgr, err := New(s, logger, gatewaySelector, c.CacheTTL, es, c.MaxConcurrency)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providerRegistryAddr := c.ProviderRegistryAddr
|
||||
if providerRegistryAddr == "" {
|
||||
providerRegistryAddr = c.GatewayAddr
|
||||
}
|
||||
mgr.RunMigrations(migration.MigrationConfig{
|
||||
ServiceAccountID: c.ServiceAccountID,
|
||||
ServiceAccountSecret: c.ServiceAccountSecret,
|
||||
ProviderRegistryAddr: providerRegistryAddr,
|
||||
})
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
// New returns a new manager instance.
|
||||
func New(s metadata.Storage, gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient], ttlSeconds int, es events.Stream, maxconcurrency int) (*Manager, error) {
|
||||
func New(s metadata.Storage,
|
||||
logger *zerolog.Logger,
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient],
|
||||
ttlSeconds int,
|
||||
es events.Stream,
|
||||
maxconcurrency int,
|
||||
) (*Manager, error) {
|
||||
if logger == nil {
|
||||
nop := zerolog.Nop()
|
||||
logger = &nop
|
||||
}
|
||||
ttl := time.Duration(ttlSeconds) * time.Second
|
||||
|
||||
m := &Manager{
|
||||
@@ -205,13 +235,38 @@ func New(s metadata.Storage, gatewaySelector pool.Selectable[gatewayv1beta1.Gate
|
||||
gatewaySelector: gatewaySelector,
|
||||
eventStream: es,
|
||||
MaxConcurrency: maxconcurrency,
|
||||
logger: logger,
|
||||
ready: make(chan struct{}),
|
||||
// migrationsDone is open (blocking) by default. It is closed by
|
||||
// doMigrations when all migrations complete, or by SkipMigrations for
|
||||
// callers (e.g. tests) that do not run migrations at all.
|
||||
migrationsDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Initialize the metadata storage connection in the background, retrying
|
||||
// with exponential backoff if the backend is not yet available.
|
||||
go func() {
|
||||
backoff := time.Second
|
||||
for {
|
||||
if err := m.initialize(context.Background()); err != nil {
|
||||
logger.Info().Err(err).Dur("backoff", backoff).Msg("share manager: metadata storage initialization failed, retrying")
|
||||
time.Sleep(backoff)
|
||||
if backoff < 30*time.Second {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
logger.Debug().Msg("share manager: initialization succeeded")
|
||||
close(m.ready)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// listen for events
|
||||
if m.eventStream != nil {
|
||||
ch, err := events.Consume(m.eventStream, "jsoncs3sharemanager", _registeredEvents...)
|
||||
if err != nil {
|
||||
appctx.GetLogger(context.Background()).Error().Err(err).Msg("error consuming events")
|
||||
logger.Error().Err(err).Msg("error consuming events")
|
||||
}
|
||||
go m.ProcessEvents(ch)
|
||||
}
|
||||
@@ -219,23 +274,13 @@ func New(s metadata.Storage, gatewaySelector pool.Selectable[gatewayv1beta1.Gate
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// initialize connects to the metadata storage backend and ensures the required
|
||||
// directory structure exists. It is called once at startup from a background
|
||||
// goroutine (see New) and must not be called concurrently.
|
||||
func (m *Manager) initialize(ctx context.Context) error {
|
||||
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "initialize")
|
||||
defer span.End()
|
||||
if m.initialized {
|
||||
span.SetStatus(codes.Ok, "already initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if m.initialized { // check if initialization happened while grabbing the lock
|
||||
span.SetStatus(codes.Ok, "initialized while grabbing lock")
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx = context.Background()
|
||||
err := m.storage.Init(ctx, "jsoncs3-share-manager-metadata")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
@@ -261,21 +306,85 @@ func (m *Manager) initialize(ctx context.Context) error {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
err = m.storage.MakeDirIfNotExist(ctx, "migrations")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
m.initialized = true
|
||||
span.SetStatus(codes.Ok, "initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForInit blocks until the background initialization goroutine has
|
||||
// successfully completed, or until ctx is cancelled.
|
||||
func (m *Manager) waitForInit(ctx context.Context) error {
|
||||
select {
|
||||
case <-m.ready:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager not yet initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// waitForMigrations blocks until both storage initialization and all data
|
||||
// migrations have completed on this instance, or until ctx is cancelled.
|
||||
// It is a strict superset of waitForInit and should be used by write operations
|
||||
// to ensure no writes race with an in-progress migration.
|
||||
func (m *Manager) waitForMigrations(ctx context.Context) error {
|
||||
select {
|
||||
case <-m.ready:
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager not yet initialized")
|
||||
}
|
||||
select {
|
||||
case <-m.migrationsDone:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager migrations not yet complete")
|
||||
}
|
||||
}
|
||||
|
||||
// RunMigrations starts data migrations in a background goroutine. It should be
|
||||
// called once after New() in production server startup. Callers that do not
|
||||
// need migrations should call SkipMigrations instead to unblock write operations.
|
||||
func (m *Manager) RunMigrations(cfg migration.MigrationConfig) {
|
||||
go m.doMigrations(cfg)
|
||||
}
|
||||
|
||||
// SkipMigrations unblocks write operations on this instance without running
|
||||
// any migrations. It must be called when RunMigrations will not be called,
|
||||
// for example in tests.
|
||||
func (m *Manager) SkipMigrations() {
|
||||
close(m.migrationsDone)
|
||||
}
|
||||
|
||||
func (m *Manager) doMigrations(cfg migration.MigrationConfig) {
|
||||
// Always close migrationsDone when this goroutine exits, whether migrations
|
||||
// ran, were skipped, or failed. This unblocks write operations on this
|
||||
// instance. Non-winning instances are held here by acquireLock until the
|
||||
// winning instance finishes, so the close happens only after the storage
|
||||
// state is fully migrated.
|
||||
defer close(m.migrationsDone)
|
||||
if err := m.waitForInit(context.Background()); err != nil {
|
||||
m.logger.Error().Err(err).Msg("share manager: aborting migrations, manager did not initialize")
|
||||
return
|
||||
}
|
||||
m.logger.Debug().Msg("migrations start")
|
||||
migrations := migration.New(*m.logger, m.gatewaySelector, m.storage, cfg, m, m)
|
||||
migrations.RunMigrations()
|
||||
}
|
||||
|
||||
func (m *Manager) ProcessEvents(ch <-chan events.Event) {
|
||||
log := logger.New()
|
||||
log := m.logger
|
||||
ctx := context.Background()
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("share manager: error waiting for initialization")
|
||||
return
|
||||
}
|
||||
for event := range ch {
|
||||
ctx := context.Background()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("error initializing manager")
|
||||
}
|
||||
|
||||
if ev, ok := event.Event.(events.SpaceDeleted); ok {
|
||||
log.Debug().Msgf("space deleted event: %v", ev)
|
||||
go func() { m.purgeSpace(ctx, ev.ID) }()
|
||||
@@ -287,7 +396,7 @@ func (m *Manager) ProcessEvents(ch <-chan events.Event) {
|
||||
func (m *Manager) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Share")
|
||||
defer span.End()
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
@@ -436,7 +545,7 @@ func (m *Manager) GetShare(ctx context.Context, ref *collaboration.ShareReferenc
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetShare")
|
||||
defer span.End()
|
||||
sublog := appctx.GetLogger(ctx).With().Str("id", ref.GetId().GetOpaqueId()).Str("key", ref.GetKey().String()).Str("driver", "jsoncs3").Str("handler", "GetShare").Logger()
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -494,7 +603,7 @@ func (m *Manager) Unshare(ctx context.Context, ref *collaboration.ShareReference
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Unshare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -511,7 +620,7 @@ func (m *Manager) UpdateShare(ctx context.Context, ref *collaboration.ShareRefer
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "UpdateShare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -599,7 +708,7 @@ func (m *Manager) ListShares(ctx context.Context, filters []*collaboration.Filte
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "ListShares")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -816,7 +925,7 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati
|
||||
defer span.End()
|
||||
sublog := appctx.GetLogger(ctx).With().Str("driver", "jsoncs3").Str("handler", "ListReceivedShares").Logger()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1012,7 +1121,7 @@ func (m *Manager) convert(ctx context.Context, userID string, s *collaboration.S
|
||||
|
||||
// GetReceivedShare returns the information for a received share.
|
||||
func (m *Manager) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1056,7 +1165,7 @@ func (m *Manager) UpdateReceivedShare(ctx context.Context, receivedShare *collab
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "UpdateReceivedShare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1103,8 +1212,8 @@ func updateShareID(share *collaboration.Share) {
|
||||
|
||||
// Load imports shares and received shares from channels (e.g. during migration)
|
||||
func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Share, receivedShareChan <-chan share.ReceivedShareWithUser) error {
|
||||
log := appctx.GetLogger(ctx)
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
l := m.logger
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1119,14 +1228,14 @@ func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Shar
|
||||
updateShareID(s)
|
||||
}
|
||||
if err := m.Cache.Add(context.Background(), s.GetResourceId().GetStorageId(), s.GetResourceId().GetSpaceId(), s.Id.OpaqueId, s); err != nil {
|
||||
log.Error().Err(err).Interface("share", s).Msg("error persisting share")
|
||||
l.Error().Err(err).Interface("share", s).Msg("error persisting share")
|
||||
} else {
|
||||
log.Debug().Str("storageid", s.GetResourceId().GetStorageId()).Str("spaceid", s.GetResourceId().GetSpaceId()).Str("shareid", s.Id.OpaqueId).Msg("imported share")
|
||||
l.Debug().Str("storageid", s.GetResourceId().GetStorageId()).Str("spaceid", s.GetResourceId().GetSpaceId()).Str("shareid", s.Id.OpaqueId).Msg("imported share")
|
||||
}
|
||||
if err := m.CreatedCache.Add(ctx, s.GetCreator().GetOpaqueId(), s.Id.OpaqueId); err != nil {
|
||||
log.Error().Err(err).Interface("share", s).Msg("error persisting created cache")
|
||||
l.Error().Err(err).Interface("share", s).Msg("error persisting created cache")
|
||||
} else {
|
||||
log.Debug().Str("creatorid", s.GetCreator().GetOpaqueId()).Str("shareid", s.Id.OpaqueId).Msg("updated created cache")
|
||||
l.Debug().Str("creatorid", s.GetCreator().GetOpaqueId()).Str("shareid", s.Id.OpaqueId).Msg("updated created cache")
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
@@ -1137,18 +1246,19 @@ func (m *Manager) Load(ctx context.Context, shareChan <-chan *collaboration.Shar
|
||||
if !shareIsRoutable(s.ReceivedShare.GetShare()) {
|
||||
updateShareID(s.ReceivedShare.GetShare())
|
||||
}
|
||||
switch s.ReceivedShare.Share.Grantee.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
if err := m.UserReceivedStates.Add(context.Background(), s.ReceivedShare.GetShare().GetGrantee().GetUserId().GetOpaqueId(), s.ReceivedShare.GetShare().GetResourceId().GetSpaceId(), s.ReceivedShare); err != nil {
|
||||
log.Error().Err(err).Interface("received share", s).Msg("error persisting received share for user")
|
||||
if s.UserID != nil {
|
||||
spaceid := s.ReceivedShare.GetShare().GetResourceId().GetStorageId() + shareid.IDDelimiter + s.ReceivedShare.GetShare().GetResourceId().GetSpaceId()
|
||||
if err := m.UserReceivedStates.Add(context.Background(), s.UserID.GetOpaqueId(), spaceid, s.ReceivedShare); err != nil {
|
||||
l.Error().Err(err).Interface("received share", s).Msg("error persisting received share for user")
|
||||
} else {
|
||||
log.Debug().Str("userid", s.ReceivedShare.GetShare().GetGrantee().GetUserId().GetOpaqueId()).Str("spaceid", s.ReceivedShare.GetShare().GetResourceId().GetSpaceId()).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share userdata")
|
||||
l.Debug().Str("userid", s.UserID.GetOpaqueId()).Str("spaceid", spaceid).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share userdata")
|
||||
}
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
}
|
||||
if s.ReceivedShare.Share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP && s.UserID == nil {
|
||||
if err := m.GroupReceivedCache.Add(context.Background(), s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId(), s.ReceivedShare.GetShare().GetId().GetOpaqueId()); err != nil {
|
||||
log.Error().Err(err).Interface("received share", s).Msg("error persisting received share to group cache")
|
||||
l.Error().Err(err).Interface("received share", s).Msg("error persisting received share to group cache")
|
||||
} else {
|
||||
log.Debug().Str("groupid", s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId()).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share group cache")
|
||||
l.Debug().Str("groupid", s.ReceivedShare.GetShare().GetGrantee().GetGroupId().GetOpaqueId()).Str("shareid", s.ReceivedShare.GetShare().Id.OpaqueId).Msg("updated received share group cache")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1220,7 +1330,7 @@ func (m *Manager) removeShare(ctx context.Context, s *collaboration.Share, skipS
|
||||
func (m *Manager) CleanupStaleShares(ctx context.Context) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if err := m.initialize(ctx); err != nil {
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+435
@@ -0,0 +1,435 @@
|
||||
// Copyright 2026 OpenCloud GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/google/uuid"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/shareid"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// storageProvider is the narrow subset of provider.ProviderAPIClient that the
|
||||
// migration actually uses. Keeping it narrow makes test stubs trivial to write.
|
||||
type storageProvider interface {
|
||||
ListGrants(ctx context.Context, in *provider.ListGrantsRequest, opts ...grpc.CallOption) (*provider.ListGrantsResponse, error)
|
||||
}
|
||||
|
||||
type ImportSpaceMembersMigration struct {
|
||||
cfg config
|
||||
sharesChan chan *collaboration.Share
|
||||
receivedChan chan share.ReceivedShareWithUser
|
||||
userCache map[string]*userpb.UserId
|
||||
groupCache map[string]*grouppb.GroupId
|
||||
providerResolver func(context.Context, *provider.StorageSpace) (storageProvider, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerMigration(&ImportSpaceMembersMigration{})
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Initialize(cfg config) {
|
||||
m.cfg = cfg
|
||||
m.sharesChan = make(chan *collaboration.Share)
|
||||
m.receivedChan = make(chan share.ReceivedShareWithUser)
|
||||
m.userCache = make(map[string]*userpb.UserId)
|
||||
m.groupCache = make(map[string]*grouppb.GroupId)
|
||||
m.providerResolver = func(ctx context.Context, space *provider.StorageSpace) (storageProvider, error) {
|
||||
return m.storageProviderForSpace(ctx, space)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Name() string {
|
||||
return "import_space_members"
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Version() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) Migrate() error {
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svcCtx, err := utils.GetServiceUserContextWithContext(context.Background(), gwc, m.cfg.serviceAccountID, m.cfg.serviceAccountSecret)
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("failed to get service user context for migration")
|
||||
return err
|
||||
}
|
||||
// List all project spaces.
|
||||
listRes, err := gwc.ListStorageSpaces(svcCtx, &provider.ListStorageSpacesRequest{
|
||||
Opaque: utils.AppendPlainToOpaque(nil, "unrestricted", "true"),
|
||||
Filters: []*provider.ListStorageSpacesRequest_Filter{
|
||||
{
|
||||
Type: provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
|
||||
Term: &provider.ListStorageSpacesRequest_Filter_SpaceType{SpaceType: "project"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("space-membership migration: failed to list storage spaces")
|
||||
return err
|
||||
}
|
||||
|
||||
if listRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
m.cfg.logger.Error().Str("status", listRes.GetStatus().GetMessage()).Msg("space-membership migration: ListStorageSpaces returned non-OK status")
|
||||
return errtypes.InternalError("ListStorageSpaces")
|
||||
}
|
||||
|
||||
spaces := listRes.GetStorageSpaces()
|
||||
m.cfg.logger.Info().Int("spaces", len(spaces)).Msg("Starting migration")
|
||||
|
||||
// loadCtx is cancelled when the producer finishes (or fails) so that the
|
||||
// Load goroutine — which blocks reading from the channels — is not left
|
||||
// waiting forever if we return early from an error.
|
||||
loadCtx, cancelLoad := context.WithCancel(svcCtx)
|
||||
defer cancelLoad()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var loaderError error
|
||||
wg.Go(func() {
|
||||
loaderError = m.cfg.loader.Load(loadCtx, m.sharesChan, m.receivedChan)
|
||||
})
|
||||
|
||||
migrated := 0
|
||||
for _, space := range spaces {
|
||||
sharesCreated, err := m.migrateSpace(loadCtx, space)
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Str("space", space.GetId().GetOpaqueId()).Msg("failed to migrate space; continuing with remaining spaces")
|
||||
continue
|
||||
}
|
||||
migrated++
|
||||
m.cfg.logger.Debug().
|
||||
Str("space", space.GetId().GetOpaqueId()).
|
||||
Int("shares_created", sharesCreated).
|
||||
Msg("space migrated")
|
||||
if migrated%10 == 0 {
|
||||
m.cfg.logger.Info().
|
||||
Int("migrated", migrated).
|
||||
Int("total", len(spaces)).
|
||||
Msg("migration progress")
|
||||
}
|
||||
}
|
||||
close(m.receivedChan)
|
||||
close(m.sharesChan)
|
||||
|
||||
wg.Wait()
|
||||
m.cfg.logger.Info().Err(loaderError).Int("migrated", migrated).Int("total", len(spaces)).Msg("Migration finished")
|
||||
return loaderError
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) migrateSpace(ctx context.Context, space *provider.StorageSpace) (int, error) {
|
||||
spClient, err := m.providerResolver(ctx, space)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ref := &provider.Reference{ResourceId: space.GetRoot()}
|
||||
grantsRes, err := spClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: ref})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if grantsRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return 0, errtypes.NewErrtypeFromStatus(grantsRes.GetStatus())
|
||||
}
|
||||
|
||||
sharesCreated := 0
|
||||
for _, grant := range grantsRes.GetGrants() {
|
||||
share, receivedShares, err := m.spaceGrantToShares(ctx, grant, space)
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).
|
||||
Interface("grant", grant).
|
||||
Msg("Failed to convert grant to shares")
|
||||
continue
|
||||
}
|
||||
if share == nil {
|
||||
// share already existed; nothing to import for this grant
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case m.sharesChan <- share:
|
||||
case <-ctx.Done():
|
||||
return sharesCreated, ctx.Err()
|
||||
}
|
||||
for _, rs := range receivedShares {
|
||||
select {
|
||||
case m.receivedChan <- rs:
|
||||
case <-ctx.Done():
|
||||
return sharesCreated, ctx.Err()
|
||||
}
|
||||
}
|
||||
sharesCreated++
|
||||
}
|
||||
return sharesCreated, nil
|
||||
}
|
||||
|
||||
// resolveRetries is the maximum number of times resolveUserID / resolveGroupID
|
||||
// will retry after receiving an errtypes.Unavailable response (LDAP down).
|
||||
const resolveRetries = 10
|
||||
|
||||
// retryOnUnavailable calls op, retrying with exponential backoff whenever op
|
||||
// returns errtypes.Unavailable. Any other error (including context
|
||||
// cancellation) stops the loop immediately and is returned as-is.
|
||||
// Retries are capped at resolveRetries attempts and respect ctx cancellation.
|
||||
func retryOnUnavailable(ctx context.Context, log zerolog.Logger, op func() error) error {
|
||||
b := backoff.WithContext(
|
||||
backoff.WithMaxRetries(backoff.NewExponentialBackOff(), resolveRetries),
|
||||
ctx,
|
||||
)
|
||||
notify := func(err error, d time.Duration) {
|
||||
log.Warn().Err(err).Dur("retry_in", d).Msg("identity provider temporarily unavailable, retrying")
|
||||
}
|
||||
return backoff.RetryNotify(func() error {
|
||||
err := op()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if _, ok := err.(errtypes.Unavailable); ok {
|
||||
return err // transient — keep retrying
|
||||
}
|
||||
return backoff.Permanent(err) // permanent — stop immediately
|
||||
}, b, notify)
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) resolveUserID(ctx context.Context, opaqueID string) (*userpb.UserId, error) {
|
||||
if id, ok := m.userCache[opaqueID]; ok {
|
||||
return id, nil
|
||||
}
|
||||
var id *userpb.UserId
|
||||
err := retryOnUnavailable(ctx, m.cfg.logger, func() error {
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := gwc.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: &userpb.UserId{OpaqueId: opaqueID},
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
// errtypes.NewErrtypeFromStatus maps CODE_UNAVAILABLE → errtypes.Unavailable,
|
||||
// which retryOnUnavailable will retry; all other codes are treated as permanent.
|
||||
return errtypes.NewErrtypeFromStatus(res.GetStatus())
|
||||
}
|
||||
id = res.GetUser().GetId()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.userCache[opaqueID] = id
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) resolveGroupID(ctx context.Context, opaqueID string) (*grouppb.GroupId, error) {
|
||||
if id, ok := m.groupCache[opaqueID]; ok {
|
||||
return id, nil
|
||||
}
|
||||
var id *grouppb.GroupId
|
||||
err := retryOnUnavailable(ctx, m.cfg.logger, func() error {
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := gwc.GetGroup(ctx, &grouppb.GetGroupRequest{
|
||||
GroupId: &grouppb.GroupId{OpaqueId: opaqueID},
|
||||
SkipFetchingMembers: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
return errtypes.NewErrtypeFromStatus(res.GetStatus())
|
||||
}
|
||||
id = res.GetGroup().GetId()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.groupCache[opaqueID] = id
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (m *ImportSpaceMembersMigration) spaceGrantToShares(ctx context.Context, grant *provider.Grant, space *provider.StorageSpace) (*collaboration.Share, []share.ReceivedShareWithUser, error) {
|
||||
// The grantee ids as persisted on disk do not have an IDP or type stored as
|
||||
// part of the userid/groupid. Resolve them via the gateway so we get the
|
||||
// full userid
|
||||
switch grant.GetGrantee().GetType() {
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
groupID, err := m.resolveGroupID(ctx, grant.GetGrantee().GetGroupId().GetOpaqueId())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("resolve group %s: %w", grant.GetGrantee().GetGroupId().GetOpaqueId(), err)
|
||||
}
|
||||
grant.Grantee.Id = &provider.Grantee_GroupId{GroupId: groupID}
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
userID, err := m.resolveUserID(ctx, grant.GetGrantee().GetUserId().GetOpaqueId())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("resolve user %s: %w", grant.GetGrantee().GetUserId().GetOpaqueId(), err)
|
||||
}
|
||||
grant.Grantee.Id = &provider.Grantee_UserId{UserId: userID}
|
||||
}
|
||||
|
||||
ref := &collaboration.ShareReference{
|
||||
Spec: &collaboration.ShareReference_Key{
|
||||
Key: &collaboration.ShareKey{
|
||||
ResourceId: space.GetRoot(),
|
||||
Grantee: grant.GetGrantee(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx = ctxpkg.ContextSetUser(ctx, &userpb.User{Id: grant.Creator})
|
||||
if s, err := m.cfg.manager.GetShare(ctx, ref); err == nil {
|
||||
// FIXME: Verify the actual grants?
|
||||
m.cfg.logger.Debug().Interface("share", s).Msg("share already exists")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
ts := utils.TSNow()
|
||||
shareID := shareid.Encode(space.GetRoot().GetStorageId(), space.GetRoot().GetSpaceId(), uuid.NewString())
|
||||
|
||||
creator := grant.GetCreator()
|
||||
if creator.Type == userpb.UserType_USER_TYPE_INVALID {
|
||||
creator = nil
|
||||
}
|
||||
newShare := &collaboration.Share{
|
||||
Id: &collaboration.ShareId{OpaqueId: shareID},
|
||||
ResourceId: space.GetRoot(),
|
||||
Permissions: &collaboration.SharePermissions{Permissions: grant.GetPermissions()},
|
||||
Grantee: grant.GetGrantee(),
|
||||
Expiration: grant.GetExpiration(),
|
||||
Owner: creator,
|
||||
Creator: creator,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}
|
||||
|
||||
var newReceivedShares []share.ReceivedShareWithUser
|
||||
switch grant.GetGrantee().GetType() {
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
gwc, err := m.cfg.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("Failed to get gateway client")
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
gr, err := gwc.GetMembers(ctx, &grouppb.GetMembersRequest{
|
||||
GroupId: grant.GetGrantee().GetGroupId(),
|
||||
})
|
||||
if err != nil {
|
||||
m.cfg.logger.Error().Err(err).Msg("Failed to expand group membership")
|
||||
return nil, nil, err
|
||||
}
|
||||
if gr.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
m.cfg.logger.Error().Str("Status", gr.GetStatus().GetMessage()).Msg("Failed to expand group membership")
|
||||
return nil, nil, errtypes.NewErrtypeFromStatus(gr.GetStatus())
|
||||
}
|
||||
for _, u := range gr.GetMembers() {
|
||||
newReceivedShares = append(newReceivedShares, share.ReceivedShareWithUser{
|
||||
UserID: u,
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: newShare,
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
})
|
||||
}
|
||||
// Also add a group-level entry (UserID == nil) so the group cache is populated.
|
||||
newReceivedShares = append(newReceivedShares, share.ReceivedShareWithUser{
|
||||
UserID: nil,
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: newShare,
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
})
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
newReceivedShares = append(newReceivedShares, share.ReceivedShareWithUser{
|
||||
UserID: grant.GetGrantee().GetUserId(),
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: newShare,
|
||||
State: collaboration.ShareState_SHARE_STATE_ACCEPTED,
|
||||
},
|
||||
})
|
||||
}
|
||||
return newShare, newReceivedShares, nil
|
||||
}
|
||||
|
||||
// storageProviderForSpace resolves the storageprovider responsible for the
|
||||
// given storage space and returns a dialled client. In the default opencloud
|
||||
// deployment the storage registry is co-located with the gateway, so
|
||||
// the GatewayAddr is used as the registry address.
|
||||
func (m *ImportSpaceMembersMigration) storageProviderForSpace(ctx context.Context, space *provider.StorageSpace) (provider.ProviderAPIClient, error) {
|
||||
|
||||
srClient, err := pool.GetStorageRegistryClient(m.cfg.providerRegistryAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get storage registry client: %w", err)
|
||||
}
|
||||
|
||||
spaceJSON, err := json.Marshal(space)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal space: %w", err)
|
||||
}
|
||||
|
||||
res, err := srClient.GetStorageProviders(ctx, ®istry.GetStorageProvidersRequest{
|
||||
Opaque: &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"space": {
|
||||
Decoder: "json",
|
||||
Value: spaceJSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetStorageProviders: %w", err)
|
||||
}
|
||||
if len(res.GetProviders()) == 0 {
|
||||
return nil, fmt.Errorf("no storage provider found for space %s", space.GetId().GetOpaqueId())
|
||||
}
|
||||
|
||||
c, err := pool.GetStorageProviderServiceClient(res.GetProviders()[0].GetAddress())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial storage provider: %w", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
Generated
Vendored
+353
@@ -0,0 +1,353 @@
|
||||
// Copyright 2026 OpenCloud GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const stateFile = "migrations/state.json"
|
||||
|
||||
const (
|
||||
lockFile = "migrations/lock.json"
|
||||
lockTTL = time.Minute
|
||||
lockHeartbeatInterval = 20 * time.Second
|
||||
)
|
||||
|
||||
// lockPollInterval is how long acquireLock sleeps between retries when the
|
||||
// lock is held by another instance. Declared as a variable so tests can
|
||||
// shorten it without rebuilding.
|
||||
var lockPollInterval = 5 * time.Second
|
||||
|
||||
// lockData is the content written to the lock file.
|
||||
type lockData struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
}
|
||||
|
||||
type migration interface {
|
||||
Name() string
|
||||
Version() int
|
||||
Initialize(config)
|
||||
Migrate() error
|
||||
}
|
||||
|
||||
// persistedState is the on-disk representation of the migration state.
|
||||
type persistedState struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type state struct {
|
||||
version int
|
||||
}
|
||||
|
||||
// MigrationConfig holds all caller-supplied options for a migration run.
|
||||
// It is intentionally a plain struct so that new fields can be added without
|
||||
// changing function signatures throughout the call chain.
|
||||
type MigrationConfig struct {
|
||||
ServiceAccountID string
|
||||
ServiceAccountSecret string
|
||||
ProviderRegistryAddr string
|
||||
}
|
||||
|
||||
type config struct {
|
||||
logger zerolog.Logger
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
|
||||
storage metadata.Storage
|
||||
serviceAccountID string
|
||||
serviceAccountSecret string
|
||||
providerRegistryAddr string
|
||||
manager share.Manager
|
||||
loader share.LoadableManager
|
||||
}
|
||||
|
||||
type Migrations struct {
|
||||
config
|
||||
state state
|
||||
instanceID string
|
||||
}
|
||||
|
||||
var migrations []migration
|
||||
|
||||
// registerMigration is only supposed to be call from init(), which runs sequentially
|
||||
// so we don't need ot protect migrations with a lock
|
||||
func registerMigration(m migration) {
|
||||
migrations = append(migrations, m)
|
||||
}
|
||||
|
||||
func New(logger zerolog.Logger,
|
||||
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient],
|
||||
storage metadata.Storage,
|
||||
cfg MigrationConfig,
|
||||
manager share.Manager,
|
||||
loader share.LoadableManager,
|
||||
) Migrations {
|
||||
|
||||
slices.SortFunc(migrations, func(a, b migration) int {
|
||||
return cmp.Compare(a.Version(), b.Version())
|
||||
})
|
||||
|
||||
b := make([]byte, 8)
|
||||
_, _ = rand.Read(b)
|
||||
instanceID := fmt.Sprintf("%x", b)
|
||||
|
||||
return Migrations{
|
||||
config{
|
||||
logger: logger.With().Str("jsoncs3", "migrations").Logger(),
|
||||
gatewaySelector: gatewaySelector,
|
||||
storage: storage,
|
||||
serviceAccountID: cfg.ServiceAccountID,
|
||||
serviceAccountSecret: cfg.ServiceAccountSecret,
|
||||
providerRegistryAddr: cfg.ProviderRegistryAddr,
|
||||
manager: manager,
|
||||
loader: loader,
|
||||
},
|
||||
state{},
|
||||
instanceID,
|
||||
}
|
||||
}
|
||||
|
||||
// acquireLock tries to atomically create the lock file, blocking until the lock
|
||||
// is obtained. It returns the etag of the lock file on success. It retries
|
||||
// indefinitely until ctx is cancelled. A lock whose timestamp is older than
|
||||
// lockTTL is considered stale and will be taken over.
|
||||
func (m *Migrations) acquireLock(ctx context.Context) (string, error) {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("acquiring migration lock")
|
||||
for {
|
||||
// Fast path: create the lock file only if it does not exist yet.
|
||||
data, err := json.Marshal(lockData{Timestamp: time.Now(), InstanceID: m.instanceID})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := m.storage.Upload(ctx, metadata.UploadRequest{
|
||||
Path: lockFile,
|
||||
Content: data,
|
||||
IfNoneMatch: []string{"*"},
|
||||
})
|
||||
if err == nil {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock acquired")
|
||||
return res.Etag, nil
|
||||
}
|
||||
|
||||
// Propagate context cancellation immediately.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Any error other than a conflict means something unexpected happened.
|
||||
if !isConflict(err) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Lock file already exists — read it to decide whether it is stale.
|
||||
dl, err := m.storage.Download(ctx, metadata.DownloadRequest{Path: lockFile})
|
||||
if err != nil {
|
||||
if _, ok := err.(errtypes.IsNotFound); ok {
|
||||
// Lock was released between our upload attempt and the download;
|
||||
// retry acquiring it immediately.
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock vanished during read; retrying")
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
var existing lockData
|
||||
stale := true
|
||||
if err := json.Unmarshal(dl.Content, &existing); err == nil {
|
||||
stale = time.Since(existing.Timestamp) > lockTTL
|
||||
}
|
||||
|
||||
if stale {
|
||||
m.logger.Debug().
|
||||
Str("instance", m.instanceID).
|
||||
Str("held_by", existing.InstanceID).
|
||||
Time("lock_timestamp", existing.Timestamp).
|
||||
Msg("migration lock is stale; attempting takeover")
|
||||
|
||||
// Atomically take over the stale lock using the etag we just read.
|
||||
newData, err := json.Marshal(lockData{Timestamp: time.Now(), InstanceID: m.instanceID})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := m.storage.Upload(ctx, metadata.UploadRequest{
|
||||
Path: lockFile,
|
||||
Content: newData,
|
||||
IfMatchEtag: dl.Etag,
|
||||
})
|
||||
if err == nil {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock acquired via stale takeover")
|
||||
return res.Etag, nil
|
||||
}
|
||||
// Another instance took the stale lock before us; loop and retry.
|
||||
m.logger.Debug().Str("instance", m.instanceID).Err(err).Msg("stale lock takeover lost race; retrying")
|
||||
continue
|
||||
}
|
||||
|
||||
m.logger.Debug().
|
||||
Str("instance", m.instanceID).
|
||||
Str("held_by", existing.InstanceID).
|
||||
Time("lock_timestamp", existing.Timestamp).
|
||||
Dur("poll_interval", lockPollInterval).
|
||||
Msg("migration lock held by another instance; waiting")
|
||||
|
||||
// Lock is fresh and held by another instance; wait before retrying.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case <-time.After(lockPollInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startHeartbeat spawns a goroutine that periodically renews the lock file so
|
||||
// that it is not considered stale while a long migration is running. Call the
|
||||
// returned cancel function to stop the heartbeat.
|
||||
func (m *Migrations) startHeartbeat(ctx context.Context, etag string) context.CancelFunc {
|
||||
hbCtx, cancel := context.WithCancel(ctx)
|
||||
go func() {
|
||||
ticker := time.NewTicker(lockHeartbeatInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-hbCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
data, err := json.Marshal(lockData{Timestamp: time.Now(), InstanceID: m.instanceID})
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("failed to marshal heartbeat data for migration lock")
|
||||
return
|
||||
}
|
||||
res, err := m.storage.Upload(hbCtx, metadata.UploadRequest{
|
||||
Path: lockFile,
|
||||
Content: data,
|
||||
IfMatchEtag: etag,
|
||||
})
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("failed to renew migration lock; another instance may take over")
|
||||
return
|
||||
}
|
||||
etag = res.Etag
|
||||
}
|
||||
}
|
||||
}()
|
||||
return cancel
|
||||
}
|
||||
|
||||
// releaseLock deletes the lock file unconditionally.
|
||||
func (m *Migrations) releaseLock(ctx context.Context) {
|
||||
if err := m.storage.Delete(ctx, lockFile); err != nil {
|
||||
m.logger.Warn().Err(err).Msg("failed to release migration lock")
|
||||
}
|
||||
}
|
||||
|
||||
// isConflict returns true for errors that signal a conditional-upload conflict,
|
||||
// i.e. the lock file already exists or the etag did not match.
|
||||
func isConflict(err error) bool {
|
||||
switch err.(type) {
|
||||
case errtypes.IsAlreadyExists, errtypes.IsAborted, errtypes.IsPreconditionFailed:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loadState reads the persisted migration version from storage. If no state
|
||||
// file exists yet (fresh deployment) it returns version 0 without error.
|
||||
func (m *Migrations) loadState(ctx context.Context) error {
|
||||
data, err := m.storage.SimpleDownload(ctx, stateFile)
|
||||
if err != nil {
|
||||
if _, ok := err.(errtypes.IsNotFound); ok {
|
||||
m.state = state{version: 0}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var ps persistedState
|
||||
if err := json.Unmarshal(data, &ps); err != nil {
|
||||
return err
|
||||
}
|
||||
m.state = state{version: ps.Version}
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveState writes the current migration version to storage so that already-
|
||||
// applied migrations are not re-run on the next server start.
|
||||
func (m *Migrations) saveState(ctx context.Context) error {
|
||||
data, err := json.Marshal(persistedState{Version: m.state.version})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.storage.SimpleUpload(ctx, stateFile, data)
|
||||
}
|
||||
|
||||
func (m *Migrations) RunMigrations() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
etag, err := m.acquireLock(ctx)
|
||||
if err != nil {
|
||||
m.logger.Error().Err(err).Msg("failed to acquire migration lock; skipping migrations")
|
||||
return
|
||||
}
|
||||
cancelHB := m.startHeartbeat(ctx, etag)
|
||||
defer cancelHB()
|
||||
defer m.releaseLock(ctx)
|
||||
|
||||
if err := m.loadState(ctx); err != nil {
|
||||
m.logger.Error().Err(err).Msg("failed to load migration state; skipping migrations")
|
||||
return
|
||||
}
|
||||
|
||||
m.logger.Info().Int("current state", m.state.version).Msg("checking migrations")
|
||||
|
||||
for _, mig := range migrations {
|
||||
if mig.Version() > m.state.version {
|
||||
m.logger.Info().Str("migration", mig.Name()).Int("version", mig.Version()).Msg("running migration")
|
||||
mig.Initialize(m.config)
|
||||
if err := mig.Migrate(); err != nil {
|
||||
m.logger.Error().Err(err).Str("migration", mig.Name()).Msg("migration failed; stopping")
|
||||
return
|
||||
}
|
||||
m.state.version = mig.Version()
|
||||
if err := m.saveState(ctx); err != nil {
|
||||
m.logger.Error().Err(err).Msg("failed to save migration state; stopping")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
m.logger.Info().Str("migration", mig.Name()).Int("version", mig.Version()).Msg("skipping migration")
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -28,6 +28,7 @@ import (
|
||||
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
@@ -46,7 +47,7 @@ func init() {
|
||||
}
|
||||
|
||||
// New returns a new manager.
|
||||
func New(c map[string]interface{}) (share.Manager, error) {
|
||||
func New(c map[string]any, _ *zerolog.Logger) (share.Manager, error) {
|
||||
state := map[string]map[*collaboration.ShareId]collaboration.ShareState{}
|
||||
mp := map[string]map[*collaboration.ShareId]*provider.Reference{}
|
||||
return &manager{
|
||||
|
||||
+5
-2
@@ -18,11 +18,14 @@
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// NewFunc is the function that share managers
|
||||
// should register at init time.
|
||||
type NewFunc func(map[string]interface{}) (share.Manager, error)
|
||||
type NewFunc func(map[string]any, *zerolog.Logger) (share.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered share managers.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ func NewNatsKeyValueFromJetStream(c Config, js jetstream.JetStream) (jetstream.K
|
||||
if err != nil {
|
||||
kvConfig := jetstream.KeyValueConfig{
|
||||
Bucket: c.Database,
|
||||
TTL: 0, // we don't do TTLs for this store
|
||||
TTL: c.TTL,
|
||||
}
|
||||
if c.DisablePersistence {
|
||||
kvConfig.Storage = jetstream.MemoryStorage
|
||||
|
||||
+5
-6
@@ -65,16 +65,16 @@ func (c *IDCache) DeleteByPath(ctx context.Context, path string) error {
|
||||
} else {
|
||||
err := c.kv.Purge(ctx, baseKey)
|
||||
if err != nil && err != nats.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", path).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not get spaceID and nodeID from cache")
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", baseKey).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
|
||||
err = c.kv.Purge(ctx, cacheKey(spaceID, nodeID))
|
||||
if err != nil && err != nats.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", path).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not get spaceID and nodeID from cache")
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", cacheKey(spaceID, nodeID)).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
}
|
||||
|
||||
watcher, err := c.kv.Watch(ctx, baseKey+".*")
|
||||
watcher, err := c.kv.Watch(ctx, baseKey+".>")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -85,7 +85,6 @@ func (c *IDCache) DeleteByPath(ctx context.Context, path string) error {
|
||||
break
|
||||
}
|
||||
key := update.Key()
|
||||
|
||||
spaceID, nodeID, ok := c.getByReverseCacheKey(ctx, key)
|
||||
if !ok {
|
||||
appctx.GetLogger(ctx).Error().Str("record", key).Msg("could not get spaceID and nodeID from cache")
|
||||
@@ -94,12 +93,12 @@ func (c *IDCache) DeleteByPath(ctx context.Context, path string) error {
|
||||
|
||||
err := c.kv.Purge(ctx, key)
|
||||
if err != nil && err != nats.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", key).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not get spaceID and nodeID from cache")
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", key).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
|
||||
err = c.kv.Purge(ctx, cacheKey(spaceID, nodeID))
|
||||
if err != nil && err != nats.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", key).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not get spaceID and nodeID from cache")
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", cacheKey(spaceID, nodeID)).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
+11
-1
@@ -24,6 +24,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
@@ -58,7 +59,8 @@ func init() {
|
||||
type posixFS struct {
|
||||
storage.FS
|
||||
|
||||
um usermapper.Mapper
|
||||
tree *tree.Tree
|
||||
um usermapper.Mapper
|
||||
}
|
||||
|
||||
// New returns an implementation to of the storage.FS interface that talk to
|
||||
@@ -70,6 +72,7 @@ func NewDefault(m map[string]interface{}, stream events.Stream, log *zerolog.Log
|
||||
}
|
||||
|
||||
o.IDCache.Database += "_v2" // Use a versioned bucket name to avoid conflicts with previous implementations
|
||||
o.IDCache.TTL = 0 // Disable TTL for the ID cache, as the posix driver relies on it for caching file IDs and we don't want them to expire
|
||||
kv, err := cache.NewNatsKeyValue(o.IDCache)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create nats key value store")
|
||||
@@ -80,6 +83,7 @@ func NewDefault(m map[string]interface{}, stream events.Stream, log *zerolog.Log
|
||||
}
|
||||
|
||||
o.IDCache.Database += "_history" // Use a versioned bucket name to avoid conflicts with previous implementations
|
||||
o.IDCache.TTL = 24 * 60 * time.Minute
|
||||
historyKv, err := cache.NewNatsKeyValue(o.IDCache)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create nats key value store")
|
||||
@@ -215,11 +219,17 @@ func New(o *options.Options, stream events.Stream, cache, historyCache *idcache.
|
||||
|
||||
mw := middleware.NewFS(dfs, hooks...)
|
||||
fs.FS = mw
|
||||
fs.tree = tp
|
||||
fs.um = um
|
||||
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// WarmupIDCache allows triggering a posix fs scan and id cache warmup manually.
|
||||
func (fs *posixFS) WarmupIDCache(root string, assimilate, onlyDirty bool) error {
|
||||
return fs.tree.WarmupIDCache(root, assimilate, onlyDirty)
|
||||
}
|
||||
|
||||
// ListUploadSessions returns the upload sessions matching the given filter
|
||||
func (fs *posixFS) ListUploadSessions(ctx context.Context, filter storage.UploadSessionFilter) ([]storage.UploadSession, error) {
|
||||
return fs.FS.(storage.UploadSessionLister).ListUploadSessions(ctx, filter)
|
||||
|
||||
-15
@@ -37,10 +37,8 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/blobstore"
|
||||
@@ -590,11 +588,6 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if appctx.DeletingSharedResourceFromContext(ctx) {
|
||||
src := filepath.Join(n.ParentPath(), n.Name)
|
||||
return os.RemoveAll(src)
|
||||
}
|
||||
|
||||
var sizeDiff int64
|
||||
if n.IsDir(ctx) {
|
||||
treesize, err := n.GetTreeSize(ctx)
|
||||
@@ -819,11 +812,3 @@ func isLockFile(path string) bool {
|
||||
func isTrash(path string) bool {
|
||||
return strings.HasSuffix(path, ".trashinfo") || strings.HasSuffix(path, ".trashitem") || strings.Contains(path, ".Trash")
|
||||
}
|
||||
|
||||
func (t *Tree) AddLabel(ctx context.Context, ref *provider.Reference, userID *user.UserId, label string) error {
|
||||
return errtypes.NotSupported("AddLabel not implemented")
|
||||
}
|
||||
|
||||
func (t *Tree) RemoveLabel(ctx context.Context, ref *provider.Reference, userID *user.UserId, label string) error {
|
||||
return errtypes.NotSupported("RemoveLabel not implemented")
|
||||
}
|
||||
|
||||
Generated
Vendored
+1
@@ -101,6 +101,7 @@ func ServiceAccountPermissions() *provider.ResourcePermissions {
|
||||
Delete: true, // for cli restore command with replace option
|
||||
CreateContainer: true, // for space provisioning
|
||||
AddGrant: true, // for initial project space member assignment
|
||||
ListGrants: true, // for initial project space member assignment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-5
@@ -429,11 +429,6 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) {
|
||||
// remove entry from cache immediately to avoid inconsistencies
|
||||
defer func() { _ = t.idCache.Delete(path) }()
|
||||
|
||||
if appctx.DeletingSharedResourceFromContext(ctx) {
|
||||
src := filepath.Join(n.ParentPath(), n.Name)
|
||||
return os.Remove(src)
|
||||
}
|
||||
|
||||
// get the original path
|
||||
origin, err := t.lookup.Path(ctx, n, node.NoCheck)
|
||||
if err != nil {
|
||||
|
||||
Generated
Vendored
-5
@@ -445,11 +445,6 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) {
|
||||
// remove entry from cache immediately to avoid inconsistencies
|
||||
defer func() { _ = t.idCache.Delete(path) }()
|
||||
|
||||
if appctx.DeletingSharedResourceFromContext(ctx) {
|
||||
src := filepath.Join(n.ParentPath(), n.Name)
|
||||
return os.Remove(src)
|
||||
}
|
||||
|
||||
// get the original path
|
||||
origin, err := t.lookup.Path(ctx, n, node.NoCheck)
|
||||
if err != nil {
|
||||
|
||||
+38
-1
@@ -93,6 +93,40 @@ func (disk *Disk) SimpleUpload(ctx context.Context, uploadpath string, content [
|
||||
// Upload stores a file on disk
|
||||
func (disk *Disk) Upload(_ context.Context, req UploadRequest) (*UploadResponse, error) {
|
||||
p := disk.targetPath(req.Path)
|
||||
|
||||
// IfNoneMatch: ["*"] means create the file only if it does not already
|
||||
// exist. Use O_EXCL so the check and the create are atomic on the local
|
||||
// filesystem.
|
||||
for _, tag := range req.IfNoneMatch {
|
||||
if tag != "*" {
|
||||
continue
|
||||
}
|
||||
f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil, errtypes.AlreadyExists(p)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if _, err := f.Write(req.Content); err != nil {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &UploadResponse{}
|
||||
res.Etag, err = calcEtag(info.ModTime(), info.Size())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if req.IfMatchEtag != "" {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
@@ -170,7 +204,10 @@ func (disk *Disk) Download(_ context.Context, req DownloadRequest) (*DownloadRes
|
||||
// SimpleDownload reads a file from disk
|
||||
func (disk *Disk) SimpleDownload(ctx context.Context, downloadpath string) ([]byte, error) {
|
||||
res, err := disk.Download(ctx, DownloadRequest{Path: downloadpath})
|
||||
return res.Content, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Content, nil
|
||||
}
|
||||
|
||||
// Delete deletes a path
|
||||
|
||||
+48
-41
@@ -266,15 +266,10 @@ func (i *Identity) GetLDAPUserByFilter(ctx context.Context, lc ldap.Client, filt
|
||||
res, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("userfilter", filter).Msg("Error looking up user by filter")
|
||||
var errmsg string
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
|
||||
errmsg = fmt.Sprintf("too many results searching for user '%s'", filter)
|
||||
}
|
||||
}
|
||||
span.SetAttributes(attribute.String("ldap.error", errmsg))
|
||||
span.SetStatus(codes.Error, errmsg)
|
||||
return nil, errtypes.NotFound(errmsg)
|
||||
classified := classifySearchError(err, fmt.Sprintf("too many results searching for user '%s'", filter))
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errtypes.NotFound(filter)
|
||||
@@ -306,9 +301,10 @@ func (i *Identity) GetLDAPUserByDN(ctx context.Context, lc ldap.Client, dn strin
|
||||
res, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("dn", dn).Msg("Error looking up user by DN")
|
||||
span.SetAttributes(attribute.String("ldap.error", err.Error()))
|
||||
span.SetStatus(codes.Error, "")
|
||||
return nil, errtypes.NotFound(dn)
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
if len(res.Entries) == 0 {
|
||||
@@ -337,9 +333,10 @@ func (i *Identity) GetLDAPUsers(ctx context.Context, lc ldap.Client, query, tena
|
||||
sr, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error searching users")
|
||||
span.SetAttributes(attribute.String("ldap.error", err.Error()))
|
||||
span.SetStatus(codes.Error, "")
|
||||
return nil, errtypes.NotFound(query)
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
|
||||
span.SetAttributes(attribute.Int("ldap.result_count", len(sr.Entries)))
|
||||
@@ -376,7 +373,8 @@ func (i *Identity) IsLDAPUserInDisabledGroup(ctx context.Context, lc ldap.Client
|
||||
sr, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Error().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error looking up error group")
|
||||
// Err on the side of caution.
|
||||
// Err on the side of caution: treat search failures (including network
|
||||
// errors) as if the user is in the disabled group.
|
||||
span.SetAttributes(attribute.String("ldap.error", err.Error()))
|
||||
span.SetStatus(codes.Error, "")
|
||||
return true
|
||||
@@ -423,10 +421,10 @@ func (i *Identity) GetLDAPUserGroups(ctx context.Context, lc ldap.Client, userEn
|
||||
// not having any groups in LDAP
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
span.SetAttributes(attribute.String("ldap.error", err.Error()))
|
||||
span.SetStatus(codes.Error, "")
|
||||
return []string{}, err
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
span.SetAttributes(attribute.Int("ldap.result_count", len(sr.Entries)))
|
||||
@@ -504,15 +502,10 @@ func (i *Identity) GetLDAPGroupByFilter(ctx context.Context, lc ldap.Client, fil
|
||||
res, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("filter", filter).Msg("Error looking up group by filter")
|
||||
var errmsg string
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
|
||||
errmsg = fmt.Sprintf("too many results searching for group '%s'", filter)
|
||||
}
|
||||
}
|
||||
span.SetAttributes(attribute.String("ldap.error", errmsg))
|
||||
span.SetStatus(codes.Error, "")
|
||||
return nil, errtypes.NotFound(errmsg)
|
||||
classified := classifySearchError(err, fmt.Sprintf("too many results searching for group '%s'", filter))
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errtypes.NotFound(filter)
|
||||
@@ -543,10 +536,11 @@ func (i *Identity) GetLDAPGroups(ctx context.Context, lc ldap.Client, query stri
|
||||
setLDAPSearchSpanAttributes(span, searchRequest)
|
||||
sr, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
span.SetAttributes(attribute.String("ldap.error", err.Error()))
|
||||
span.SetStatus(codes.Error, "")
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("query", query).Msg("Error search for groups")
|
||||
return nil, errtypes.NotFound(query)
|
||||
classified := classifySearchError(err, "")
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
span.SetStatus(codes.Ok, "")
|
||||
return sr.Entries, nil
|
||||
@@ -919,15 +913,10 @@ func (i *Identity) GetLDAPTenantByFilter(ctx context.Context, lc ldap.Client, fi
|
||||
res, err := lc.Search(searchRequest)
|
||||
if err != nil {
|
||||
log.Debug().Str("backend", "ldap").Err(err).Str("tenantfilter", filter).Msg("Error looking up tenant by filter")
|
||||
var errmsg string
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
|
||||
errmsg = fmt.Sprintf("too many results searching for tenant '%s'", filter)
|
||||
}
|
||||
}
|
||||
span.SetAttributes(attribute.String("ldap.error", errmsg))
|
||||
span.SetStatus(codes.Error, errmsg)
|
||||
return nil, errtypes.NotFound(errmsg)
|
||||
classified := classifySearchError(err, fmt.Sprintf("too many results searching for tenant '%s'", filter))
|
||||
span.SetAttributes(attribute.String("ldap.error", classified.Error()))
|
||||
span.SetStatus(codes.Error, classified.Error())
|
||||
return nil, classified
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, errtypes.NotFound(filter)
|
||||
@@ -980,6 +969,24 @@ func (i *Identity) getTenantAttributeFilter(attribute, value string) (string, er
|
||||
), nil
|
||||
}
|
||||
|
||||
// classifySearchError maps a raw error from lc.Search to the appropriate
|
||||
// errtypes value:
|
||||
// - ldap.ErrorNetwork → errtypes.Unavailable (transient; caller should retry)
|
||||
// - ldap.LDAPResultSizeLimitExceeded → errtypes.NotFound(sizeExceededMsg)
|
||||
// - anything else → errtypes.NotFound("") (preserving prior behaviour)
|
||||
//
|
||||
// The sizeExceededMsg is only used for the SizeLimitExceeded case; pass an
|
||||
// empty string if the caller does not need a custom message for that case.
|
||||
func classifySearchError(err error, sizeExceededMsg string) error {
|
||||
if ldap.IsErrorWithCode(err, ldap.ErrorNetwork) {
|
||||
return errtypes.Unavailable("ldap server unreachable: " + err.Error())
|
||||
}
|
||||
if sizeExceededMsg != "" && ldap.IsErrorWithCode(err, ldap.LDAPResultSizeLimitExceeded) {
|
||||
return errtypes.NotFound(sizeExceededMsg)
|
||||
}
|
||||
return errtypes.NotFound("")
|
||||
}
|
||||
|
||||
func setLDAPSearchSpanAttributes(span trace.Span, request *ldap.SearchRequest) {
|
||||
span.SetAttributes(
|
||||
attribute.String("ldap.basedn", request.BaseDN),
|
||||
|
||||
Reference in New Issue
Block a user