mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 20:26:45 +00:00
* s3: apply filer identity changes despite a static config file A -config file with inline identities disabled the metadata-subscription reload entirely, leaving the best-effort filer->s3 push as the only way s3.configure changes could reach a running gateway. Reload on IAM events regardless: the merge keeps the file's identities protected, and a full credential-manager snapshot now also drops dynamic identities the store no longer has, so revocation works without a restart. * s3: log identity propagation failures as warnings * s3: retry failed IAM reloads and reconcile policies and groups An event-driven reload that fails now hands off to a coalescing retry loop, so a transient filer error cannot strand a revoked credential until the next IAM event. Full-state merges also drop dynamic policies the store no longer has, keeping the static file's, and treat the group snapshot as authoritative even when empty. * s3: serialize IAM configuration loads The SIGHUP file reload, subscription reloads, the retry loop, and the postgres poll run on different goroutines. Without an end-to-end lock a load holding an older store snapshot can commit after a newer one and revert it. Hold reloadMu from snapshot through commit in both load entry points; partial merges from pushed updates stay lock-free and self-heal through the next event-driven reload. * s3: keep static-file groups through full-state reconciliation Group names from the static config file are tracked like identities and policies, and a full snapshot that does not carry them keeps the current definition and its memberships instead of dropping them. * credential: include groups in postgres configuration snapshots Full-state reconciliation treats absent groups as deleted, so a snapshot that never carries them would erase every dynamic group. * s3: revoke static-file groups dropped from the config file A file reload is authoritative for the file's group set while keeping dynamic groups, mirroring how full snapshots are authoritative for dynamic groups while keeping the file's. * credential: fail filer snapshots on unreadable entries A skipped identity or policy file made the load report success with an incomplete snapshot, which reconciliation reads as deletion and the retry loop never sees. Unparseable content is still skipped: it is durable, matches boot behavior, and must not block reloads forever. * s3: ignore groups in static config files Groups are managed through the IAM API and the dynamic store; no deployment defines them in a bootstrap config file. Ignoring them with a warning removes the two-directional group merge: full snapshots are plainly authoritative and file reloads never touch groups.
244 lines
9.8 KiB
Go
244 lines
9.8 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
const oidcProvidersDir = filer.IamConfigDirectory + "/oidc-providers"
|
|
|
|
func (s3a *S3ApiServer) subscribeMetaEvents(clientName string, lastTsNs int64, prefix string, directoriesToWatch []string) {
|
|
|
|
processEventFn := func(resp *filer_pb.SubscribeMetadataResponse) error {
|
|
|
|
message := resp.EventNotification
|
|
|
|
// For rename/move operations, NewParentPath contains the destination directory.
|
|
// We process both source and destination dirs so moves out of watched
|
|
// directories (e.g., IAM config dirs) are not missed.
|
|
dir := resp.Directory
|
|
if message.NewParentPath != "" {
|
|
dir = message.NewParentPath
|
|
}
|
|
|
|
// Handle all metadata changes (create, update, delete, rename)
|
|
// These handlers check for nil entries internally
|
|
_ = s3a.onBucketMetadataChange(dir, message.OldEntry, message.NewEntry)
|
|
_ = s3a.onIamConfigChange(dir, message.OldEntry, message.NewEntry)
|
|
_ = s3a.onOIDCProviderChange(dir, message.OldEntry, message.NewEntry)
|
|
_ = s3a.onCircuitBreakerConfigChange(dir, message.OldEntry, message.NewEntry)
|
|
|
|
// For moves across directories, replay a delete event for the source directory
|
|
if message.NewParentPath != "" && resp.Directory != message.NewParentPath {
|
|
_ = s3a.onBucketMetadataChange(resp.Directory, message.OldEntry, nil)
|
|
_ = s3a.onIamConfigChange(resp.Directory, message.OldEntry, nil)
|
|
_ = s3a.onOIDCProviderChange(resp.Directory, message.OldEntry, nil)
|
|
_ = s3a.onCircuitBreakerConfigChange(resp.Directory, message.OldEntry, nil)
|
|
}
|
|
|
|
// For same-directory renames, replay a delete event for the old name
|
|
// so handlers can clean up stale state (e.g., old bucket names)
|
|
if message.OldEntry != nil && message.NewEntry != nil &&
|
|
(message.NewParentPath == "" || message.NewParentPath == resp.Directory) &&
|
|
message.OldEntry.Name != message.NewEntry.Name {
|
|
_ = s3a.onBucketMetadataChange(dir, message.OldEntry, nil)
|
|
_ = s3a.onCircuitBreakerConfigChange(dir, message.OldEntry, nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
metadataFollowOption := &pb.MetadataFollowOption{
|
|
ClientName: clientName,
|
|
ClientId: s3a.randomClientId,
|
|
ClientEpoch: 1,
|
|
SelfSignature: 0,
|
|
PathPrefix: prefix,
|
|
AdditionalPathPrefixes: nil,
|
|
DirectoriesToWatch: directoriesToWatch,
|
|
StartTsNs: lastTsNs,
|
|
StopTsNs: 0,
|
|
EventErrorType: pb.FatalOnError,
|
|
}
|
|
util.RetryUntil("followIamChanges", func() error {
|
|
metadataFollowOption.ClientEpoch++
|
|
return pb.WithFilerClientFollowMetadata(s3a, metadataFollowOption, processEventFn)
|
|
}, func(err error) bool {
|
|
glog.V(1).Infof("iam follow metadata changes: %v", err)
|
|
return true
|
|
})
|
|
}
|
|
|
|
// onIamConfigChange handles IAM config file changes (create, update, delete).
|
|
// It reloads even with a static -config file: the merge protects the file's
|
|
// identities, and the filer->s3 push alone is best-effort.
|
|
func (s3a *S3ApiServer) onIamConfigChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
|
|
if s3a.iam == nil {
|
|
return nil
|
|
}
|
|
|
|
reloadIamConfig := func(reason string) error {
|
|
glog.V(1).Infof("IAM change detected in %s, reloading configuration", reason)
|
|
if err := s3a.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
|
|
// the event stream moves on; retry state-based until a reload succeeds
|
|
glog.Errorf("failed to reload IAM configuration after change in %s: %v", reason, err)
|
|
s3a.iam.scheduleReload()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 1. Handle traditional single identity.json file
|
|
if dir == filer.IamConfigDirectory {
|
|
// Handle create/update/delete events on legacy identity.json.
|
|
// During migration this file is renamed, which emits a delete event.
|
|
// Always reload from the credential manager so we keep the migrated identities.
|
|
if (oldEntry != nil && oldEntry.Name == filer.IamIdentityFile) ||
|
|
(newEntry != nil && newEntry.Name == filer.IamIdentityFile) {
|
|
if err := reloadIamConfig(dir + "/" + filer.IamIdentityFile); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 2. Handle multiple-file identities and policies
|
|
// Watch /etc/iam/{identities,policies,service_accounts}
|
|
isIdentityDir := dir == filer.IamConfigDirectory+"/identities" || strings.HasPrefix(dir, filer.IamConfigDirectory+"/identities/")
|
|
isPolicyDir := dir == filer.IamConfigDirectory+"/policies" || strings.HasPrefix(dir, filer.IamConfigDirectory+"/policies/")
|
|
isServiceAccountDir := dir == filer.IamConfigDirectory+"/service_accounts" || strings.HasPrefix(dir, filer.IamConfigDirectory+"/service_accounts/")
|
|
isGroupDir := dir == filer.IamConfigDirectory+"/groups" || strings.HasPrefix(dir, filer.IamConfigDirectory+"/groups/")
|
|
|
|
if isIdentityDir || isPolicyDir || isServiceAccountDir || isGroupDir {
|
|
// For multiple-file mode, any change in these directories should trigger a full reload
|
|
// from the credential manager (which handles the details of loading from multiple files).
|
|
if err := reloadIamConfig(dir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// onOIDCProviderChange refreshes the IAM-managed OIDC provider runtime view
|
|
// whenever the persisted store under /etc/iam/oidc-providers changes — both
|
|
// for mutations originated on this S3 server (the local IAM API also calls
|
|
// RefreshOIDCProvidersFromStore inline, but the subscribe path costs nothing
|
|
// extra) and for mutations originated on peer S3 servers, which this is the
|
|
// only mechanism to learn about. A single refresh covers create, update,
|
|
// delete, and rename because the store is small and a full reload is the
|
|
// safest way to reach a consistent view.
|
|
func (s3a *S3ApiServer) onOIDCProviderChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
|
|
if dir != oidcProvidersDir && !strings.HasPrefix(dir, oidcProvidersDir+"/") {
|
|
return nil
|
|
}
|
|
if s3a.iam == nil || s3a.iam.iamIntegration == nil {
|
|
return nil
|
|
}
|
|
s3iam, ok := s3a.iam.iamIntegration.(*S3IAMIntegration)
|
|
if !ok || s3iam.iamManager == nil {
|
|
return nil
|
|
}
|
|
if err := s3iam.iamManager.RefreshOIDCProvidersFromStore(context.Background()); err != nil {
|
|
glog.Warningf("OIDC provider refresh after %s change failed: %v", dir, err)
|
|
return err
|
|
}
|
|
glog.V(2).Infof("Refreshed IAM-managed OIDC providers after %s change", dir)
|
|
return nil
|
|
}
|
|
|
|
// onCircuitBreakerConfigChange handles circuit breaker config file changes (create, update, delete)
|
|
func (s3a *S3ApiServer) onCircuitBreakerConfigChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
|
|
if dir != s3_constants.CircuitBreakerConfigDir {
|
|
return nil
|
|
}
|
|
|
|
// Handle deletion: reset to empty config
|
|
if newEntry == nil && oldEntry != nil && oldEntry.Name == s3_constants.CircuitBreakerConfigFile {
|
|
glog.V(1).Infof("Circuit breaker config file deleted, resetting to defaults")
|
|
if err := s3a.cb.LoadS3ApiConfigurationFromBytes([]byte{}); err != nil {
|
|
glog.Warningf("failed to reset circuit breaker config on deletion: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handle create/update
|
|
if newEntry != nil && newEntry.Name == s3_constants.CircuitBreakerConfigFile {
|
|
if err := s3a.cb.LoadS3ApiConfigurationFromBytes(newEntry.Content); err != nil {
|
|
return err
|
|
}
|
|
glog.V(1).Infof("updated %s/%s", dir, newEntry.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// reload bucket metadata
|
|
func (s3a *S3ApiServer) onBucketMetadataChange(dir string, oldEntry *filer_pb.Entry, newEntry *filer_pb.Entry) error {
|
|
if dir == s3a.option.BucketsPath {
|
|
s3a.maintainBucketOwnerIndex(oldEntry, newEntry)
|
|
if newEntry != nil {
|
|
// Update bucket registry (existing functionality)
|
|
s3a.bucketRegistry.LoadBucketMetadata(newEntry)
|
|
glog.V(1).Infof("updated bucketMetadata %s/%s", dir, newEntry.Name)
|
|
|
|
// Update bucket configuration cache with new entry
|
|
s3a.updateBucketConfigCacheFromEntry(newEntry)
|
|
} else if oldEntry != nil {
|
|
// Remove from bucket registry (existing functionality)
|
|
s3a.bucketRegistry.RemoveBucketMetadata(oldEntry)
|
|
glog.V(1).Infof("remove bucketMetadata %s/%s", dir, oldEntry.Name)
|
|
|
|
// Remove from bucket configuration cache
|
|
s3a.invalidateBucketConfigCache(oldEntry.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// updateBucketConfigCacheFromEntry updates the bucket config cache when a bucket entry changes
|
|
func (s3a *S3ApiServer) updateBucketConfigCacheFromEntry(entry *filer_pb.Entry) {
|
|
if s3a.bucketConfigCache == nil {
|
|
return
|
|
}
|
|
|
|
bucket := entry.Name
|
|
|
|
// Remove from negative cache since bucket now exists
|
|
// This is important for buckets created via weed shell or other external means
|
|
s3a.bucketConfigCache.RemoveNegativeCache(bucket)
|
|
|
|
// Only refresh buckets already resident in the cache; cold buckets
|
|
// lazy-load on first access so the cache holds this gateway's working
|
|
// set, not every bucket in the cluster.
|
|
if !s3a.bucketConfigCache.Contains(bucket) {
|
|
return
|
|
}
|
|
|
|
// newBucketConfigFromEntry is the single source of truth for mapping
|
|
// Entry.Extended → cached fields (incl. LifecycleTTL), so a meta-log
|
|
// Put/DeleteBucketLifecycle here can't leave a stale resolver in cache.
|
|
config := s3a.newBucketConfigFromEntry(bucket, entry)
|
|
|
|
glog.V(3).Infof("updateBucketConfigCacheFromEntry: refreshing cache for bucket %s, ObjectLockConfig=%+v", bucket, config.ObjectLockConfig)
|
|
s3a.bucketConfigCache.Set(bucket, config)
|
|
}
|
|
|
|
// invalidateBucketConfigCache removes a bucket from the configuration cache
|
|
func (s3a *S3ApiServer) invalidateBucketConfigCache(bucket string) {
|
|
if s3a.bucketConfigCache == nil {
|
|
return
|
|
}
|
|
|
|
s3a.bucketConfigCache.Remove(bucket)
|
|
s3a.bucketConfigCache.RemoveNegativeCache(bucket) // Also remove from negative cache
|
|
glog.V(2).Infof("invalidateBucketConfigCache: removed bucket %s from cache", bucket)
|
|
}
|