credential: migrate the legacy IAM file without overwriting newer entries

The migration wrote every identity, service account and group from the
snapshot the load read, over whatever the per-entry files held by then, so
a revoke or delete landing in between was undone for good. It now migrates
only the legacy file's own entries, creates each per-entry file only when
none exists, and waits for the next load when the legacy file changed
after the snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 committed 2026-11-13 02:52:41 -08:00
1 parent a576398662
commit 6f10339dd2
1 file changed
+63 -13
+63 -13
View File
@@ -1,10 +1,14 @@
package filer_etc
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/filer"
@@ -54,7 +58,7 @@ func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
// This ensures that all identities (including legacy ones) are written to individual files
// and the legacy file is renamed.
if foundLegacy {
if err := store.migrateToMultiFile(ctx, s3cfg); err != nil {
if err := store.migrateToMultiFile(ctx, content); err != nil {
glog.Errorf("Failed to migrate IAM configuration to multi-file layout: %v", err)
return s3cfg, nil
}
@@ -137,26 +141,43 @@ func (store *FilerEtcStore) loadFromMultiFile(ctx context.Context, s3cfg *iam_pb
return hasIdentities, nil
}
func (store *FilerEtcStore) migrateToMultiFile(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration) error {
func (store *FilerEtcStore) migrateToMultiFile(ctx context.Context, legacyContent []byte) error {
current, found, err := store.readInsideFiler(ctx, filer.IamConfigDirectory, IamLegacyIdentityFile)
if err != nil {
return err
}
if !found || !bytes.Equal(current, legacyContent) {
glog.V(0).Infof("%s changed during the IAM load, migrating it at the next load", IamLegacyIdentityFile)
return nil
}
legacy := &iam_pb.S3ApiConfiguration{}
if err := filer.ParseS3ConfigurationFromBytes(current, legacy); err != nil {
return err
}
glog.Infof("Migrating IAM configuration to multi-file layout...")
// 1. Save all identities
for _, identity := range s3cfg.Identities {
if err := store.saveIdentity(ctx, identity); err != nil {
// An existing per-entry file is newer than the legacy copy, and may have
// been rewritten or removed after this load read it: never overwrite it.
for _, identity := range legacy.Identities {
if err := store.createIfAbsent(ctx, IamIdentitiesDirectory, identity.Name, identity); err != nil {
return err
}
}
// 2. Save all service accounts
for _, sa := range s3cfg.ServiceAccounts {
if err := store.saveServiceAccount(ctx, sa); err != nil {
for _, sa := range legacy.ServiceAccounts {
if err := validateServiceAccountId(sa.Id); err != nil {
return err
}
if err := store.createIfAbsent(ctx, IamServiceAccountsDirectory, sa.Id, sa); err != nil {
return err
}
}
// 3. Save all groups
for _, g := range s3cfg.Groups {
if err := store.saveGroup(ctx, g); err != nil {
for _, g := range legacy.Groups {
g.Name = strings.TrimSpace(g.Name)
if g.Name == "" {
return fmt.Errorf("group name is required")
}
if err := store.createIfAbsent(ctx, IamGroupsDirectory, g.Name, g); err != nil {
return err
}
}
@@ -487,6 +508,35 @@ func (store *FilerEtcStore) saveIdentity(ctx context.Context, identity *iam_pb.I
})
}
func (store *FilerEtcStore) createIfAbsent(ctx context.Context, subDir, name string, v any) error {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
contentMd5 := md5.Sum(data)
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
err := filer_pb.CreateEntry(ctx, client, &filer_pb.CreateEntryRequest{
Directory: filer.IamConfigDirectory + "/" + subDir,
Entry: &filer_pb.Entry{
Name: name + ".json",
Content: data,
Attributes: &filer_pb.FuseAttributes{
Mtime: time.Now().Unix(),
Crtime: time.Now().Unix(),
FileMode: uint32(0644),
FileSize: uint64(len(data)),
Md5: contentMd5[:],
},
},
OExcl: true,
})
if errors.Is(err, filer_pb.ErrEntryAlreadyExists) {
return nil
}
return err
})
}
func (store *FilerEtcStore) readInsideFiler(ctx context.Context, dir string, name string) ([]byte, bool, error) {
var content []byte
found := false