chore: bump reva to latest main

This commit is contained in:
Jannik Stehle committed 2026-08-18 15:25:23 +02:00
1 parent c2a01e3aad
commit c198c2e453
30 files changed
+621 -17463

No files matched your search

+2 -2
View File
@@ -64,7 +64,7 @@ require (
github.com/open-policy-agent/opa v1.19.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260818072644-485a511e30c2
github.com/opensearch-project/opensearch-go/v4 v4.6.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
@@ -94,7 +94,7 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0
go-micro.dev/v4 v4.11.0
go.etcd.io/bbolt v1.5.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0
go.opentelemetry.io/contrib/zpages v0.69.0
go.opentelemetry.io/otel v1.45.0
+4 -4
View File
@@ -942,8 +942,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55 h1:dzYZ5iA6i0QoFUap9l6sZ4Ts2HXiXVfsOoiRYToYgm8=
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96 h1:2yPjf1jhrhEkrHsX8s6gAQVogBmNY6ZTtPNOtLfc1p8=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96/go.mod h1:+2IJFVwi3yBEBfw0T+jgY7M9LBSPv7ecPYDiG3/bWYg=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260818072644-485a511e30c2 h1:OKUkfXQYOlBIFFiOg4I2w/p2J+OSjvTt+Oeb6HQQQG0=
github.com/opencloud-eu/reva/v2 v2.48.1-0.20260818072644-485a511e30c2/go.mod h1:ReY9Pc8omXotf3Phn+nsKGalppMsG6kt0mpnKcgKmMU=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -1292,8 +1292,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 h1:oECp5f+hN7nkwjU/8BxQ/q23bGPb8FIrD839owX222E=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0/go.mod h1:DqEFwLumhzMBDQv9PcWbyoDxHI/4lAk6CM4nJBH39sc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
go.opentelemetry.io/contrib/zpages v0.69.0 h1:YQC1PumJq6lUGQNrLW14ID9a0dXSBcbC/aC6VRSIARU=
+338
View File
@@ -0,0 +1,338 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package expirable
import (
"sync"
"time"
"github.com/hashicorp/golang-lru/v2/internal"
)
// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback[K comparable, V any] func(key K, value V)
// LRU implements a thread-safe LRU with expirable entries.
type LRU[K comparable, V any] struct {
size int
evictList *internal.LruList[K, V]
items map[K]*internal.Entry[K, V]
onEvict EvictCallback[K, V]
// expirable options
mu sync.Mutex
ttl time.Duration
done chan struct{}
// buckets for expiration
buckets []bucket[K, V]
// uint8 because it's number between 0 and numBuckets
nextCleanupBucket uint8
}
// bucket is a container for holding entries to be expired
type bucket[K comparable, V any] struct {
entries map[K]*internal.Entry[K, V]
newestEntry time.Time
}
// noEvictionTTL - very long ttl to prevent eviction
const noEvictionTTL = time.Hour * 24 * 365 * 10
// because of uint8 usage for nextCleanupBucket, should not exceed 256.
// casting it as uint8 explicitly requires type conversions in multiple places
const numBuckets = 100
// NewLRU returns a new thread-safe cache with expirable entries.
//
// Size parameter set to 0 makes cache of unlimited size, e.g. turns LRU mechanism off.
//
// Providing 0 TTL turns expiring off.
//
// Delete expired entries every 1/100th of ttl value. Goroutine which deletes expired entries runs indefinitely.
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl time.Duration) *LRU[K, V] {
if size < 0 {
size = 0
}
if ttl <= 0 {
ttl = noEvictionTTL
}
res := LRU[K, V]{
ttl: ttl,
size: size,
evictList: internal.NewList[K, V](),
items: make(map[K]*internal.Entry[K, V]),
onEvict: onEvict,
done: make(chan struct{}),
}
// initialize the buckets
res.buckets = make([]bucket[K, V], numBuckets)
for i := 0; i < numBuckets; i++ {
res.buckets[i] = bucket[K, V]{entries: make(map[K]*internal.Entry[K, V])}
}
// enable deleteExpired() running in separate goroutine for cache with non-zero TTL
//
// Important: done channel is never closed, so deleteExpired() goroutine will never exit,
// it's decided to add functionality to close it in the version later than v2.
if res.ttl != noEvictionTTL {
go func(done <-chan struct{}) {
ticker := time.NewTicker(res.ttl / numBuckets)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
res.deleteExpired()
}
}
}(res.done)
}
return &res
}
// Purge clears the cache completely.
// onEvict is called for each evicted key.
func (c *LRU[K, V]) Purge() {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.items {
if c.onEvict != nil {
c.onEvict(k, v.Value)
}
delete(c.items, k)
}
for _, b := range c.buckets {
for _, ent := range b.entries {
delete(b.entries, ent.Key)
}
}
c.evictList.Init()
}
// Add adds a value to the cache. Returns true if an eviction occurred.
// Returns false if there was no eviction: the item was already in the cache,
// or the size was not exceeded.
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
c.removeFromBucket(ent) // remove the entry from its current bucket as expiresAt is renewed
ent.Value = value
ent.ExpiresAt = now.Add(c.ttl)
c.addToBucket(ent)
return false
}
// Add new item
ent := c.evictList.PushFrontExpirable(key, value, now.Add(c.ttl))
c.items[key] = ent
c.addToBucket(ent) // adds the entry to the appropriate bucket and sets entry.expireBucket
evict := c.size > 0 && c.evictList.Length() > c.size
// Verify size not exceeded
if evict {
c.removeOldest()
}
return evict
}
// Get looks up a key's value from the cache.
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
// Expired item check
if time.Now().After(ent.ExpiresAt) {
return value, false
}
c.evictList.MoveToFront(ent)
return ent.Value, true
}
return
}
// Contains checks if a key is in the cache, without updating the recent-ness
// or deleting it for being stale.
func (c *LRU[K, V]) Contains(key K) (ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
_, ok = c.items[key]
return ok
}
// Peek returns the key value (or undefined if not found) without updating
// the "recently used"-ness of the key.
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
// Expired item check
if time.Now().After(ent.ExpiresAt) {
return value, false
}
return ent.Value, true
}
return
}
// Remove removes the provided key from the cache, returning if the
// key was contained.
func (c *LRU[K, V]) Remove(key K) bool {
c.mu.Lock()
defer c.mu.Unlock()
if ent, ok := c.items[key]; ok {
c.removeElement(ent)
return true
}
return false
}
// RemoveOldest removes the oldest item from the cache.
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
return ent.Key, ent.Value, true
}
return
}
// GetOldest returns the oldest entry
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if ent := c.evictList.Back(); ent != nil {
return ent.Key, ent.Value, true
}
return
}
// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *LRU[K, V]) Keys() []K {
c.mu.Lock()
defer c.mu.Unlock()
keys := make([]K, 0, len(c.items))
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
keys = append(keys, ent.Key)
}
return keys
}
// Values returns a slice of the values in the cache, from oldest to newest.
// Expired entries are filtered out.
func (c *LRU[K, V]) Values() []V {
c.mu.Lock()
defer c.mu.Unlock()
values := make([]V, len(c.items))
i := 0
now := time.Now()
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
if now.After(ent.ExpiresAt) {
continue
}
values[i] = ent.Value
i++
}
return values
}
// Len returns the number of items in the cache.
func (c *LRU[K, V]) Len() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.evictList.Length()
}
// Resize changes the cache size. Size of 0 means unlimited.
func (c *LRU[K, V]) Resize(size int) (evicted int) {
c.mu.Lock()
defer c.mu.Unlock()
if size <= 0 {
c.size = 0
return 0
}
diff := c.evictList.Length() - size
if diff < 0 {
diff = 0
}
for i := 0; i < diff; i++ {
c.removeOldest()
}
c.size = size
return diff
}
// Close destroys cleanup goroutine. To clean up the cache, run Purge() before Close().
// func (c *LRU[K, V]) Close() {
// c.mu.Lock()
// defer c.mu.Unlock()
// select {
// case <-c.done:
// return
// default:
// }
// close(c.done)
// }
// removeOldest removes the oldest item from the cache. Has to be called with lock!
func (c *LRU[K, V]) removeOldest() {
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
}
}
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
c.evictList.Remove(e)
delete(c.items, e.Key)
c.removeFromBucket(e)
if c.onEvict != nil {
c.onEvict(e.Key, e.Value)
}
}
// deleteExpired deletes expired records from the oldest bucket, waiting for the newest entry
// in it to expire first.
func (c *LRU[K, V]) deleteExpired() {
c.mu.Lock()
bucketIdx := c.nextCleanupBucket
timeToExpire := time.Until(c.buckets[bucketIdx].newestEntry)
// wait for newest entry to expire before cleanup without holding lock
if timeToExpire > 0 {
c.mu.Unlock()
time.Sleep(timeToExpire)
c.mu.Lock()
}
for _, ent := range c.buckets[bucketIdx].entries {
c.removeElement(ent)
}
c.nextCleanupBucket = (c.nextCleanupBucket + 1) % numBuckets
c.mu.Unlock()
}
// addToBucket adds entry to expire bucket so that it will be cleaned up when the time comes. Has to be called with lock!
func (c *LRU[K, V]) addToBucket(e *internal.Entry[K, V]) {
bucketID := (numBuckets + c.nextCleanupBucket - 1) % numBuckets
e.ExpireBucket = bucketID
c.buckets[bucketID].entries[e.Key] = e
if c.buckets[bucketID].newestEntry.Before(e.ExpiresAt) {
c.buckets[bucketID].newestEntry = e.ExpiresAt
}
}
// removeFromBucket removes the entry from its corresponding bucket. Has to be called with lock!
func (c *LRU[K, V]) removeFromBucket(e *internal.Entry[K, V]) {
delete(c.buckets[e.ExpireBucket].entries, e.Key)
}
+14
View File
@@ -53,6 +53,20 @@ func (re *Event) Ack() error {
return re.msg.Ack()
}
func (re *Event) Nak() error {
if re.msg == nil {
return errors.New("cannot nack event without message")
}
return re.msg.Nak()
}
func (re *Event) Term() error {
if re.msg == nil {
return errors.New("cannot terminate event without message")
}
return re.msg.Term()
}
func (re *Event) InProgress() error {
if re.msg == nil {
return errors.New("cannot mark event as in progress without message")
@@ -82,6 +82,7 @@ var (
events.PostprocessingFinished{},
events.PostprocessingStepFinished{},
events.RestartPostprocessing{},
events.StartPostprocessingStep{},
}
)
@@ -441,6 +442,22 @@ func (fs *Decomposedfs) Postprocessing(ch <-chan events.Event) {
}); err != nil {
sublog.Error().Err(err).Msg("Failed to publish BytesReceived event")
}
case events.StartPostprocessingStep:
sublog := log.With().Str("event", "StartPostprocessingStep").Str("uploadid", ev.UploadID).Logger()
if ev.UploadID == "" {
sublog.Error().Msg("UploadID is empty, cannot start postprocessing step")
continue
}
session, err := fs.sessionStore.Get(ctx, ev.UploadID)
if err != nil {
sublog.Error().Err(err).Msg("Failed to get upload")
continue
}
session.SetStatus(upload.SessionStatusProcessing, "started postprocessing step: "+string(ev.StepToStart))
err = session.Persist(ctx)
if err != nil {
sublog.Error().Err(err).Msg("Failed to persist upload session after starting postprocessing step")
}
case events.PostprocessingStepFinished:
sublog := log.With().Str("event", "PostprocessingStepFinished").Str("uploadid", ev.UploadID).Logger()
if ev.FinishedStep != events.PPStepAntivirus {
@@ -102,6 +102,7 @@ const (
SpaceImageAttr string = OcPrefix + "space.image"
SpaceAliasAttr string = OcPrefix + "space.alias"
SpaceTenantIDAttr string = OcPrefix + "space.tenantid"
SpaceContentTypeAttr string = OcPrefix + "space.contenttype"
UserAcePrefix string = "u:"
GroupAcePrefix string = "g:"
@@ -99,6 +99,7 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr
}
description := utils.ReadPlainFromOpaque(req.Opaque, "description")
contentType := utils.ReadPlainFromOpaque(req.Opaque, "contentType")
alias := utils.ReadPlainFromOpaque(req.Opaque, "spaceAlias")
if alias == "" {
alias = templates.WithSpacePropertiesAndUser(u, req.Type, req.Name, spaceID, fs.o.GeneralSpaceAliasTemplate)
@@ -192,6 +193,10 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr
metadata.SetString(prefixes.SpaceDescriptionAttr, description)
}
if contentType != "" {
metadata.SetString(prefixes.SpaceContentTypeAttr, contentType)
}
if alias != "" {
metadata.SetString(prefixes.SpaceAliasAttr, alias)
}
@@ -607,6 +612,9 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up
if description, ok := space.Opaque.Map["description"]; ok {
metadata[prefixes.SpaceDescriptionAttr] = description.Value
}
if contentType, ok := space.Opaque.Map["contentType"]; ok {
metadata[prefixes.SpaceContentTypeAttr] = contentType.Value
}
if alias := utils.ReadPlainFromOpaque(space.Opaque, "spaceAlias"); alias != "" {
metadata.SetString(prefixes.SpaceAliasAttr, alias)
}
@@ -660,7 +668,7 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up
if !permissions.IsManager(sp) {
// We are not a space manager. We need to check for additional permissions.
k := []string{prefixes.NameAttr, prefixes.SpaceDescriptionAttr}
k := []string{prefixes.NameAttr, prefixes.SpaceDescriptionAttr, prefixes.SpaceContentTypeAttr}
if !permissions.IsEditor(sp) {
k = append(k, prefixes.SpaceReadmeAttr, prefixes.SpaceAliasAttr, prefixes.SpaceImageAttr)
}
@@ -1118,6 +1126,9 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node,
if sd := spaceAttributes.String(prefixes.SpaceDescriptionAttr); sd != "" {
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "description", sd)
}
if se := spaceAttributes.String(prefixes.SpaceContentTypeAttr); se != "" {
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "contentType", se)
}
if sr := spaceAttributes.String(prefixes.SpaceReadmeAttr); sr != "" {
space.Opaque = utils.AppendPlainToOpaque(space.Opaque, "readme", storagespace.FormatResourceID(
&provider.ResourceId{StorageId: space.Root.StorageId, SpaceId: space.GetRoot().GetSpaceId(), OpaqueId: sr},
@@ -46,6 +46,12 @@ type DecomposedFsSession struct {
info tusd.FileInfo
}
const (
SessionStatusUploading = "uploading"
SessionStatusProcessing = "processing"
SessionStatusFailed = "failed"
)
// Context returns a context with the user, logger and lockid used when initiating the upload session
func (session *DecomposedFsSession) Context(ctx context.Context) context.Context { // restore logger from file info
sub := session.store.log.With().Int("pid", os.Getpid()).Logger()
@@ -307,10 +313,9 @@ func (session *DecomposedFsSession) MTime() time.Time {
return t
}
// IsProcessing returns true if all bytes have been received. The session then has entered postprocessing state.
// IsProcessing returns true if the upload is in the processing state, meaning that the upload has finished and postprocessing is still running.
func (session *DecomposedFsSession) IsProcessing() bool {
// We might need a more sophisticated way to determine processing status soon
return session.info.Size == session.info.Offset && session.info.MetaData["scanResult"] == ""
return session.info.MetaData["status"] == SessionStatusProcessing
}
// binPath returns the path to the file storing the binary data.
@@ -339,6 +344,22 @@ func (session *DecomposedFsSession) ScanData() (string, time.Time) {
return session.info.MetaData["scanResult"], d
}
// SetStatus sets the status of the upload session
func (session *DecomposedFsSession) SetStatus(status, msg string) {
session.info.MetaData["status"] = status
session.info.MetaData["statusMessage"] = msg
}
// Status returns the status of the upload session
func (session *DecomposedFsSession) Status() string {
return session.info.MetaData["status"]
}
// StatusMessage returns the status message of the upload session
func (session *DecomposedFsSession) StatusMessage() string {
return session.info.MetaData["statusMessage"]
}
// sessionPath returns the path to the .info file storing the file's info.
func sessionPath(root, id string) string {
return filepath.Join(root, "uploads", id+".info")
@@ -94,7 +94,9 @@ func (store DecomposedFsStore) New(ctx context.Context) *DecomposedFsSession {
Storage: map[string]string{
"Type": "DecomposedFsStore",
},
MetaData: tusd.MetaData{},
MetaData: tusd.MetaData{
"status": string(SessionStatusUploading),
},
},
}
}
@@ -194,8 +194,18 @@ func (session *DecomposedFsSession) FinishUploadDecomposed(ctx context.Context)
n, err := session.store.CreateNodeForUpload(ctx, session, attrs)
if err != nil {
session.SetStatus(SessionStatusFailed, err.Error())
if perr := session.Persist(ctx); perr != nil {
log.Error().Err(perr).Msg("failed to persist upload session after setting status to failed")
}
return err
}
session.SetStatus(SessionStatusProcessing, "")
if err = session.Persist(ctx); err != nil {
log.Error().Err(err).Msg("failed to persist upload session after setting status to processing")
}
// increase the processing counter for every started processing
// will be decreased in Cleanup()
metrics.UploadProcessing.Inc()
+5
View File
@@ -94,6 +94,11 @@ type UploadSession interface {
// ScanData returns the scan data for the UploadSession
ScanData() (string, time.Time)
// Status returns the status for the UploadSession
Status() string
// StatusMessage returns the status message for the UploadSession
StatusMessage() string
}
// UploadSessionFilter can be used to filter upload sessions
@@ -338,6 +338,22 @@ func (s *OcisSession) ScanData() (string, time.Time) {
return s.info.MetaData["scanResult"], d
}
func (s *OcisSession) SetStatus(status string) {
s.info.MetaData["status"] = status
}
func (s *OcisSession) Status() string {
return s.info.MetaData["status"]
}
func (s *OcisSession) SetStatusMessage(message string) {
s.info.MetaData["status_message"] = message
}
func (s *OcisSession) StatusMessage() string {
return s.info.MetaData["status_message"]
}
// sessionPath returns the path to the .info file storing the file's info.
func sessionPath(root, id string) string {
return filepath.Join(root, "uploads", id+".info")
+87 -4
View File
@@ -23,10 +23,12 @@ import (
"errors"
"fmt"
"strings"
"time"
identityUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
@@ -38,12 +40,45 @@ import (
// Identity provides methods to query users and groups from an LDAP server
type Identity struct {
User userConfig `mapstructure:",squash"`
Group groupConfig `mapstructure:",squash"`
Tenant tenantConfig `mapstructure:",squash"`
User userConfig `mapstructure:",squash"`
Group groupConfig `mapstructure:",squash"`
Tenant tenantConfig `mapstructure:",squash"`
LookupCacheTTL string `mapstructure:"lookup_cache_ttl"`
lookupCache entryCache
}
const tracerName = "pkg/utils/ldap"
// entryCache is a small nil-safe wrapper around the expirable LRU used to cache
// LDAP lookups. A zero-value entryCache (nil lru) is a valid, disabled cache:
// Get always misses and Add is a no-op. This lets the cache be turned off
// entirely (e.g. in acceptance tests) by configuring a lookup_cache_ttl of 0.
type entryCache struct {
lru *expirable.LRU[string, *ldap.Entry]
}
// Get returns the cached entry for key. It reports a miss when the cache is
// disabled (nil lru).
func (c entryCache) Get(key string) (*ldap.Entry, bool) {
if c.lru == nil {
return nil, false
}
return c.lru.Get(key)
}
// Add stores entry under key. It is a no-op when the cache is disabled (nil lru).
func (c entryCache) Add(key string, entry *ldap.Entry) {
if c.lru == nil {
return
}
c.lru.Add(key, entry)
}
const (
tracerName = "pkg/utils/ldap"
lookupCacheDefaultTTL = 10 * time.Second
lookupCacheSize = 1024
)
type userConfig struct {
BaseDN string `mapstructure:"user_base_dn"`
@@ -223,6 +258,27 @@ func (i *Identity) Setup() error {
}
}
if i.LookupCacheTTL != "" {
// Parse the TTL string if provided
parsedTTL, err := time.ParseDuration(i.LookupCacheTTL)
if err != nil {
return fmt.Errorf("error parsing lookup_cache_ttl %q: %w", i.LookupCacheTTL, err)
}
switch {
case parsedTTL < 0:
return fmt.Errorf("error configuring lookup cache ttl: duration must be >= 0")
case parsedTTL == 0:
// A TTL of 0 disables the lookup cache entirely. A zero-value
// entryCache always misses and never stores anything.
i.lookupCache = entryCache{}
default:
i.lookupCache = entryCache{lru: expirable.NewLRU[string, *ldap.Entry](lookupCacheSize, nil, parsedTTL)}
}
} else {
// Use default TTL if not provided
i.lookupCache = entryCache{lru: expirable.NewLRU[string, *ldap.Entry](lookupCacheSize, nil, lookupCacheDefaultTTL)}
}
return nil
}
@@ -254,6 +310,12 @@ func (i *Identity) GetLDAPUserByFilter(ctx context.Context, lc ldap.Client, filt
log := appctx.GetLogger(ctx)
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUserByFilter")
defer span.End()
cacheKey := fmt.Sprintf("user:filter:%s", filter)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
searchRequest := ldap.NewSearchRequest(
i.User.BaseDN, i.User.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
@@ -275,6 +337,7 @@ func (i *Identity) GetLDAPUserByFilter(ctx context.Context, lc ldap.Client, filt
return nil, errtypes.NotFound(filter)
}
span.SetStatus(codes.Ok, "")
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -286,6 +349,11 @@ func (i *Identity) GetLDAPUserByDN(ctx context.Context, lc ldap.Client, dn strin
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPUserByDN")
defer span.End()
cacheKey := fmt.Sprintf("user:dn:%s", dn)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
filter := fmt.Sprintf("(objectclass=%s)", i.User.Objectclass)
if i.User.Filter != "" {
filter = fmt.Sprintf("(&%s%s)", i.User.Filter, filter)
@@ -310,6 +378,7 @@ func (i *Identity) GetLDAPUserByDN(ctx context.Context, lc ldap.Client, dn strin
if len(res.Entries) == 0 {
return nil, errtypes.NotFound(dn)
}
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -483,6 +552,12 @@ func (i *Identity) GetLDAPGroupByFilter(ctx context.Context, lc ldap.Client, fil
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPGroupByFilter")
defer span.End()
log := appctx.GetLogger(ctx)
cacheKey := fmt.Sprintf("group:filter:%s", filter)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
searchRequest := ldap.NewSearchRequest(
i.Group.BaseDN, i.Group.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
@@ -511,6 +586,7 @@ func (i *Identity) GetLDAPGroupByFilter(ctx context.Context, lc ldap.Client, fil
return nil, errtypes.NotFound(filter)
}
span.SetStatus(codes.Ok, "")
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -901,6 +977,12 @@ func (i *Identity) GetLDAPTenantByFilter(ctx context.Context, lc ldap.Client, fi
log := appctx.GetLogger(ctx)
_, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "GetLDAPTenantByFilter")
defer span.End()
cacheKey := fmt.Sprintf("tenant:filter:%s", filter)
if cached, ok := i.lookupCache.Get(cacheKey); ok {
return cached, nil
}
searchRequest := ldap.NewSearchRequest(
i.Tenant.BaseDN, i.Tenant.scopeVal, ldap.NeverDerefAliases, 1, 0, false,
filter,
@@ -922,6 +1004,7 @@ func (i *Identity) GetLDAPTenantByFilter(ctx context.Context, lc ldap.Client, fi
return nil, errtypes.NotFound(filter)
}
span.SetStatus(codes.Ok, "")
i.lookupCache.Add(cacheKey, res.Entries[0])
return res.Entries[0], nil
}
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"context"
@@ -8,4 +8,4 @@ Use [NewClientHandler] with [grpc.WithStatsHandler] to instrument a gRPC client.
Use [NewServerHandler] with [grpc.StatsHandler] to instrument a gRPC server.
*/
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
@@ -1,21 +1,62 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
// gRPC tracing middleware
// https://opentelemetry.io/docs/specs/semconv/rpc/
import (
"net"
"net/url"
"strconv"
"strings"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
grpc_codes "google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// serverAddrAttrsFromCanonicalTarget extracts server address attributes from a
// canonical gRPC target as returned by [google.golang.org/grpc.ClientConn.CanonicalTarget].
// Canonical targets always have the form "scheme://[authority]/endpoint", e.g.:
//
// passthrough:///127.0.0.1:7777 → ServerAddress("127.0.0.1"), ServerPort(7777)
// dns:///example.com:443 → ServerAddress("example.com"), ServerPort(443)
// dns://authority/example.com:443 → ServerAddress("example.com"), ServerPort(443)
// unix:///tmp/grpc.sock → ServerAddress("/tmp/grpc.sock")
func serverAddrAttrsFromCanonicalTarget(target string) []attribute.KeyValue {
// Fast path: confirmed host:port with numeric port, no scheme involved.
if h, pStr, err := net.SplitHostPort(target); err == nil {
if p, err := strconv.Atoi(pStr); err == nil {
return []attribute.KeyValue{
semconv.ServerAddress(h),
semconv.ServerPort(p),
}
}
}
// gRPC URI: scheme://authority/endpoint → endpoint in Path
// scheme:endpoint → endpoint in Opaque
u, err := url.Parse(target)
if err != nil {
return []attribute.KeyValue{semconv.ServerAddress(target)}
}
ep := u.Path
if u.Scheme != "unix" && u.Scheme != "unix-abstract" {
// Strip the leading "/" added by url.Parse for hierarchical URIs;
// preserve it for unix socket paths where the slash is meaningful.
ep = strings.TrimPrefix(ep, "/")
}
if ep == "" {
ep = u.Opaque
}
if ep != "" {
return serverAddrAttrs(ep)
}
return []attribute.KeyValue{semconv.ServerAddress(target)}
}
// serverAddrAttrs returns the server address attributes for the hostport.
func serverAddrAttrs(hostport string) []attribute.KeyValue {
h, pStr, err := net.SplitHostPort(hostport)
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"google.golang.org/grpc"
@@ -2,14 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
// Package internal provides internal functionality for the otelgrpc package.
package internal // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal"
package internal
import (
"strings"
"go.opentelemetry.io/otel/attribute"
oldsemconv "go.opentelemetry.io/otel/semconv/v1.37.0" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
)
// ParseFullMethod returns a span name following the OpenTelemetry semantic
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"context"
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
import (
"context"
@@ -13,8 +13,8 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
oldrpcconv "go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv"
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
"go.opentelemetry.io/otel/semconv/v1.43.0/rpcconv"
"go.opentelemetry.io/otel/trace"
grpc_codes "google.golang.org/grpc/codes"
@@ -26,6 +26,11 @@ import (
type gRPCContextKey struct{}
// dialTargetContextKey carries the dial target seeded by client interceptors
// so the stats handler can use the hostname for server.address instead of the
// post-DNS IP from RemoteAddr.
type dialTargetContextKey struct{}
type gRPCContext struct {
metricAttrs []attribute.KeyValue
record bool
@@ -328,6 +333,20 @@ func (*config) handleRPC(
switch rs := rs.(type) {
case *stats.Begin:
// Set server.address early from the dial target when available (upcoming
// interceptors will seed this). Covers both success and failure paths since Begin
// fires on every RPC regardless of outcome.
if rs.Client {
if target, ok := ctx.Value(dialTargetContextKey{}).(string); ok && target != "" {
attrs := serverAddrAttrsFromCanonicalTarget(target)
if span.IsRecording() {
span.SetAttributes(attrs...)
}
if gctx != nil {
gctx.metricAttrs = append(gctx.metricAttrs, attrs...)
}
}
}
case *stats.InPayload:
case *stats.InHeader:
if !rs.Client && rs.LocalAddr != nil {
@@ -339,13 +358,18 @@ func (*config) handleRPC(
case *stats.OutPayload:
case *stats.OutTrailer:
case *stats.OutHeader:
// Only use the resolved IP from RemoteAddr when no dial target was seeded
// (i.e. NewClientHandler callers without interceptors). When dialTargetContextKey
// is present, Begin already set server.address to the hostname.
if rs.Client && rs.RemoteAddr != nil && (span.IsRecording() || gctx != nil) {
attrs := serverAddrAttrs(rs.RemoteAddr.String())
if span.IsRecording() {
span.SetAttributes(attrs...)
}
if gctx != nil {
gctx.metricAttrs = append(gctx.metricAttrs, attrs...)
if target, ok := ctx.Value(dialTargetContextKey{}).(string); !ok || target == "" {
attrs := serverAddrAttrs(rs.RemoteAddr.String())
if span.IsRecording() {
span.SetAttributes(attrs...)
}
if gctx != nil {
gctx.metricAttrs = append(gctx.metricAttrs, attrs...)
}
}
}
case *stats.End:
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
package otelgrpc
// Version is the current release version of the gRPC instrumentation.
const Version = "0.69.0"
const Version = "0.70.0"
-17
View File
@@ -1,17 +0,0 @@
<!-- Generated. DO NOT MODIFY. -->
# Migration from v1.40.0 to v1.41.0
The `go.opentelemetry.io/otel/semconv/v1.41.0` package should be a drop-in replacement for `go.opentelemetry.io/otel/semconv/v1.40.0` with the following exceptions.
## Removed
The following declarations have been removed.
Refer to the [OpenTelemetry Semantic Conventions documentation] for deprecation instructions.
If the type is not listed in the documentation as deprecated, it has been removed in this version due to lack of applicability or use.
If you use any of these non-deprecated declarations in your Go application, please [open an issue] describing your use-case.
- `DeploymentEnvironmentName`
[OpenTelemetry Semantic Conventions documentation]: https://github.com/open-telemetry/semantic-conventions
[open an issue]: https://github.com/open-telemetry/opentelemetry-go/issues/new?template=Blank+issue
-3
View File
@@ -1,3 +0,0 @@
# Semconv v1.41.0
[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/semconv/v1.41.0)](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.41.0)
File diff suppressed because it is too large. Load diff
-11
View File
@@ -1,11 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package semconv implements OpenTelemetry semantic conventions.
//
// OpenTelemetry semantic conventions are agreed standardized naming
// patterns for OpenTelemetry things. This package represents the v1.41.0
// version of the OpenTelemetry semantic conventions.
package semconv
-83
View File
@@ -1,83 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package semconv
import (
"errors"
"fmt"
"reflect"
"go.opentelemetry.io/otel/attribute"
)
// ErrorType returns an [attribute.KeyValue] identifying the error type of err.
//
// If err is nil, the returned attribute has the default value
// [ErrorTypeOther].
//
// If err or one of the errors in its chain has the method
//
// ErrorType() string
//
// the returned attribute has that method's return value. If multiple errors in
// the chain implement this method, the value from the first match found by
// [errors.As] is used. Otherwise, the returned attribute has a value derived
// from the concrete type of err after unwrapping any wrappers created with
// [fmt.Errorf].
//
// The key of the returned attribute is [ErrorTypeKey].
func ErrorType(err error) attribute.KeyValue {
if err == nil {
return ErrorTypeOther
}
return ErrorTypeKey.String(errorType(err))
}
func errorType(err error) string {
var s string
if et, ok := err.(interface{ ErrorType() string }); ok {
// Fast path: check the top-level error first.
s = et.ErrorType()
} else {
// Fallback: search the error chain for an ErrorType method.
var et interface{ ErrorType() string }
if errors.As(err, &et) {
// Prioritize the ErrorType method if available.
s = et.ErrorType()
}
}
if s == "" {
// Fallback to reflection if the ErrorType method is not supported or
// returns an empty value.
t := reflect.TypeOf(unwrapFmtWrapped(err))
pkg, name := t.PkgPath(), t.Name()
if pkg != "" && name != "" {
s = pkg + "." + name
} else {
// The type has no package path or name (predeclared, not-defined,
// or alias for a not-defined type).
//
// This is not guaranteed to be unique, but is a best effort.
s = t.String()
}
}
return s
}
var fmtWrapErrorType = reflect.TypeOf(fmt.Errorf("wrapped: %w", errors.New("err")))
func unwrapFmtWrapped(err error) error {
for reflect.TypeOf(err) == fmtWrapErrorType {
u := errors.Unwrap(err)
if u == nil {
return err // When the wrapped error is nil, use the concrete type of the wrapper.
}
err = u
}
return err
}
-11
View File
@@ -1,11 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package semconv
const (
// ExceptionEventName is the name of the Span event representing an exception.
ExceptionEventName = "exception"
)
-11
View File
@@ -1,11 +0,0 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package semconv
// SchemaURL is the schema URL that matches the version of the semantic conventions
// that this package defines. Semconv packages starting from v1.4.0 must declare
// non-empty schema URL in the form https://opentelemetry.io/schemas/<version>
const SchemaURL = "https://opentelemetry.io/schemas/1.41.0"
+4 -4
View File
@@ -796,6 +796,7 @@ github.com/hashicorp/go-plugin/runner
# github.com/hashicorp/golang-lru/v2 v2.0.7
## explicit; go 1.18
github.com/hashicorp/golang-lru/v2
github.com/hashicorp/golang-lru/v2/expirable
github.com/hashicorp/golang-lru/v2/internal
github.com/hashicorp/golang-lru/v2/simplelru
# github.com/hashicorp/yamux v0.1.2
@@ -1364,7 +1365,7 @@ github.com/opencloud-eu/icap-client
# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260818063016-69f82a7dde55
## explicit; go 1.23
github.com/opencloud-eu/libre-graph-api-go
# github.com/opencloud-eu/reva/v2 v2.48.1-0.20260811071808-9bf68d2ccd96
# github.com/opencloud-eu/reva/v2 v2.48.1-0.20260818072644-485a511e30c2
## explicit; go 1.25.8
github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace
github.com/opencloud-eu/reva/v2/cmd/revad/runtime
@@ -2299,7 +2300,7 @@ go.opencensus.io/trace/tracestate
## explicit; go 1.24.0
go.opentelemetry.io/auto/sdk
go.opentelemetry.io/auto/sdk/internal/telemetry
# go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0
# go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0
## explicit; go 1.25.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal
@@ -2330,11 +2331,10 @@ go.opentelemetry.io/otel/semconv/v1.20.0
go.opentelemetry.io/otel/semconv/v1.20.0/httpconv
go.opentelemetry.io/otel/semconv/v1.37.0
go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv
go.opentelemetry.io/otel/semconv/v1.41.0
go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv
go.opentelemetry.io/otel/semconv/v1.43.0
go.opentelemetry.io/otel/semconv/v1.43.0/httpconv
go.opentelemetry.io/otel/semconv/v1.43.0/otelconv
go.opentelemetry.io/otel/semconv/v1.43.0/rpcconv
# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0
## explicit; go 1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace