1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 05:19:39 +02:00
portainer/pkg/libhelm/cache/cache.go

126 lines
3.8 KiB
Go

package cache
import (
"fmt"
"time"
"github.com/patrickmn/go-cache"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/registry"
)
// Cache manages Helm registry clients with TTL-based expiration
// Registry clients are cached per registry ID rather than per user session
// to optimize rate limiting - one login per registry per Portainer instance
type Cache struct {
cache *cache.Cache
}
// CachedRegistryClient wraps a registry client with metadata
type CachedRegistryClient struct {
Client *registry.Client
RegistryID portainer.RegistryID
CreatedAt time.Time
}
// newCache creates a new Helm registry client cache with the specified timeout
func newCache(userSessionTimeout string) (*Cache, error) {
timeout, err := time.ParseDuration(userSessionTimeout)
if err != nil {
return nil, fmt.Errorf("invalid user session timeout: %w", err)
}
return &Cache{
cache: cache.New(timeout, timeout),
}, nil
}
// getByRegistryID retrieves a cached registry client by registry ID
// Cache key strategy: use registryID for maximum efficiency against rate limits
// This means one login per registry per Portainer instance, regardless of user/environment
func (c *Cache) getByRegistryID(registryID portainer.RegistryID) (*registry.Client, bool) {
key := generateRegistryIDCacheKey(registryID)
cachedClient, found := c.cache.Get(key)
if !found {
log.Debug().
Str("cache_key", key).
Int("registry_id", int(registryID)).
Str("context", "HelmRegistryCache").
Msg("Cache miss for registry client")
return nil, false
}
client := cachedClient.(CachedRegistryClient)
log.Debug().
Str("cache_key", key).
Int("registry_id", int(registryID)).
Str("context", "HelmRegistryCache").
Msg("Cache hit for registry client")
return client.Client, true
}
// setByRegistryID stores a registry client in the cache with registry ID context
func (c *Cache) setByRegistryID(registryID portainer.RegistryID, client *registry.Client) {
if client == nil {
log.Warn().
Int("registry_id", int(registryID)).
Str("context", "HelmRegistryCache").
Msg("Attempted to cache nil registry client")
return
}
key := generateRegistryIDCacheKey(registryID)
cachedClient := CachedRegistryClient{
Client: client,
RegistryID: registryID,
CreatedAt: time.Now(),
}
c.cache.Set(key, cachedClient, cache.DefaultExpiration)
log.Debug().
Str("cache_key", key).
Int("registry_id", int(registryID)).
Str("context", "HelmRegistryCache").
Msg("Cached registry client")
}
// flushRegistry removes cached registry client for a specific registry ID
// This should be called whenever registry credentials change
func (c *Cache) flushRegistry(registryID portainer.RegistryID) {
key := generateRegistryIDCacheKey(registryID)
c.cache.Delete(key)
log.Info().
Int("registry_id", int(registryID)).
Str("context", "HelmRegistryCache").
Msg("Flushed registry client due to registry change")
}
// flushAll removes all cached registry clients
func (c *Cache) flushAll() {
itemCount := c.cache.ItemCount()
c.cache.Flush()
if itemCount > 0 {
log.Info().
Int("cached_clients_removed", itemCount).
Str("context", "HelmRegistryCache").
Msg("Flushed all registry clients")
}
}
// generateRegistryIDCacheKey creates a cache key from registry ID
// Key strategy decision: Use registry ID instead of user sessions or URL+username
// This provides optimal rate limiting protection since each registry only gets
// logged into once per Portainer instance, regardless of how many users access it
// RBAC security is enforced before reaching this caching layer
// When a new user needs access, they reuse the same cached client
func generateRegistryIDCacheKey(registryID portainer.RegistryID) string {
return fmt.Sprintf("registry:%d", registryID)
}