From b1d63d094327ac3114fdcca5a46c593104f593fe Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 27 Jan 2026 19:50:16 +0100 Subject: [PATCH] feat(shell): add Object Lock management commands (#8141) * feat(shell): add s3.bucket.lock command for Object Lock management Add new weed shell command to view and enable S3 Object Lock on existing buckets. This allows administrators to enable Object Lock without recreating buckets, which is useful when buckets already contain data. The command: - Shows current Object Lock and Versioning status - Enables Object Lock with -enable flag (irreversible, per AWS S3 spec) - Automatically enables Versioning if not already enabled (required for Object Lock) Usage: s3.bucket.lock -name # view status s3.bucket.lock -name -enable # enable Object Lock Co-Authored-By: Claude Signed-off-by: Andrei Kvapil * feat(shell): add -withLock flag to s3.bucket.create command Add support for creating buckets with Object Lock enabled directly from weed shell. The flag automatically enables versioning as required by Object Lock. Usage: s3.bucket.create -name mybucket -withLock Co-Authored-By: Claude Signed-off-by: Andrei Kvapil * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Signed-off-by: Andrei Kvapil Co-authored-by: Claude Co-authored-by: Chris Lu Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- weed/shell/command_s3_bucket_create.go | 21 ++++ weed/shell/command_s3_bucket_lock.go | 152 +++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 weed/shell/command_s3_bucket_lock.go diff --git a/weed/shell/command_s3_bucket_create.go b/weed/shell/command_s3_bucket_create.go index ee6d3ec6a..5fcc7e7fb 100644 --- a/weed/shell/command_s3_bucket_create.go +++ b/weed/shell/command_s3_bucket_create.go @@ -31,6 +31,7 @@ func (c *commandS3BucketCreate) Help() string { Example: s3.bucket.create -name s3.bucket.create -name -owner + s3.bucket.create -name -withLock The -owner flag sets the bucket owner identity. This is important when using S3 IAM authentication, as non-admin users can only access buckets they own. @@ -39,6 +40,10 @@ func (c *commandS3BucketCreate) Help() string { The -owner value should match the identity name configured in your S3 IAM system (the "name" field in s3.json identities configuration). + + The -withLock flag enables S3 Object Lock on the bucket. This provides WORM + (Write Once Read Many) protection for objects. Once enabled, Object Lock + cannot be disabled. Versioning is automatically enabled when using this flag. ` } @@ -51,6 +56,7 @@ func (c *commandS3BucketCreate) Do(args []string, commandEnv *CommandEnv, writer bucketCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) bucketName := bucketCommand.String("name", "", "bucket name") bucketOwner := bucketCommand.String("owner", "", "bucket owner identity name (for S3 IAM authentication)") + withLock := bucketCommand.Bool("withLock", false, "enable Object Lock on the bucket (requires and enables versioning)") if err = bucketCommand.Parse(args); err != nil { return nil } @@ -95,6 +101,17 @@ func (c *commandS3BucketCreate) Do(args []string, commandEnv *CommandEnv, writer entry.Extended[s3_constants.AmzIdentityId] = []byte(owner) } + // Enable Object Lock if specified + if *withLock { + if entry.Extended == nil { + entry.Extended = make(map[string][]byte) + } + // Enable versioning (required for Object Lock) + entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled) + // Enable Object Lock + entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(s3_constants.ObjectLockEnabled) + } + if _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ Directory: filerBucketsPath, Entry: entry, @@ -106,6 +123,10 @@ func (c *commandS3BucketCreate) Do(args []string, commandEnv *CommandEnv, writer if owner != "" { fmt.Fprintln(writer, "bucket owner:", owner) } + if *withLock { + fmt.Fprintln(writer, "Object Lock: enabled") + fmt.Fprintln(writer, "Versioning: enabled") + } return nil diff --git a/weed/shell/command_s3_bucket_lock.go b/weed/shell/command_s3_bucket_lock.go new file mode 100644 index 000000000..9cbeda6e1 --- /dev/null +++ b/weed/shell/command_s3_bucket_lock.go @@ -0,0 +1,152 @@ +package shell + +import ( + "context" + "flag" + "fmt" + "io" + + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/util" +) + +func init() { + Commands = append(Commands, &commandS3BucketLock{}) +} + +type commandS3BucketLock struct { +} + +func (c *commandS3BucketLock) Name() string { + return "s3.bucket.lock" +} + +func (c *commandS3BucketLock) Help() string { + return `view or enable Object Lock for an S3 bucket + + Example: + # View the current Object Lock status of a bucket + s3.bucket.lock -name + + # Enable Object Lock on an existing bucket (irreversible) + s3.bucket.lock -name -enable + + Object Lock provides WORM (Write Once Read Many) protection for objects. + Once enabled, Object Lock cannot be disabled on a bucket. + + Requirements: + - Versioning will be automatically enabled if not already enabled + - Object Lock can only be enabled, never disabled (following AWS S3 behavior) + + After enabling Object Lock, you can: + - Set default retention policy on the bucket + - Set retention and legal hold on individual objects +` +} + +func (c *commandS3BucketLock) HasTag(CommandTag) bool { + return false +} + +func (c *commandS3BucketLock) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) { + + bucketCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + bucketName := bucketCommand.String("name", "", "bucket name") + enableLock := bucketCommand.Bool("enable", false, "enable Object Lock on the bucket (irreversible)") + if err = bucketCommand.Parse(args); err != nil { + return err + } + + if *bucketName == "" { + return fmt.Errorf("empty bucket name") + } + + err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { + + resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{}) + if err != nil { + return fmt.Errorf("get filer configuration: %w", err) + } + filerBucketsPath := resp.DirBuckets + + // Look up the bucket entry + lookupResp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ + Directory: filerBucketsPath, + Name: *bucketName, + }) + if err != nil { + return fmt.Errorf("lookup bucket %s: %w", *bucketName, err) + } + + entry := lookupResp.Entry + + // Check current Object Lock status + currentLockEnabled := false + currentVersioningEnabled := false + if entry.Extended != nil { + if lockStatus, ok := entry.Extended[s3_constants.ExtObjectLockEnabledKey]; ok { + currentLockEnabled = string(lockStatus) == s3_constants.ObjectLockEnabled + } + if versioningStatus, ok := entry.Extended[s3_constants.ExtVersioningKey]; ok { + currentVersioningEnabled = string(versioningStatus) == s3_constants.VersioningEnabled + } + } + + // If -enable is provided, enable Object Lock + if *enableLock { + if currentLockEnabled { + fmt.Fprintf(writer, "Object Lock is already enabled on bucket %s\n", *bucketName) + return nil + } + + if entry.Extended == nil { + entry.Extended = make(map[string][]byte) + } + + // Enable versioning if not already enabled (required for Object Lock) + if !currentVersioningEnabled { + entry.Extended[s3_constants.ExtVersioningKey] = []byte(s3_constants.VersioningEnabled) + fmt.Fprintf(writer, "Enabling versioning on bucket %s (required for Object Lock)\n", *bucketName) + } + + // Enable Object Lock + entry.Extended[s3_constants.ExtObjectLockEnabledKey] = []byte(s3_constants.ObjectLockEnabled) + fmt.Fprintf(writer, "Enabling Object Lock on bucket %s\n", *bucketName) + + // Update the entry + if _, err := client.UpdateEntry(context.Background(), &filer_pb.UpdateEntryRequest{ + Directory: filerBucketsPath, + Entry: entry, + }); err != nil { + return fmt.Errorf("failed to update bucket: %w", err) + } + + fmt.Fprintf(writer, "Object Lock enabled successfully.\n") + fmt.Fprintf(writer, "WARNING: This action is irreversible. Object Lock cannot be disabled.\n") + return nil + } + + // Display current status (no flags provided) + fmt.Fprintf(writer, "Bucket: %s\n", *bucketName) + fmt.Fprintf(writer, "Path: %s\n", util.NewFullPath(filerBucketsPath, *bucketName)) + + if currentVersioningEnabled { + fmt.Fprintf(writer, "Versioning: Enabled\n") + } else { + fmt.Fprintf(writer, "Versioning: Disabled\n") + } + + if currentLockEnabled { + fmt.Fprintf(writer, "Object Lock: Enabled\n") + } else { + fmt.Fprintf(writer, "Object Lock: Disabled\n") + fmt.Fprintf(writer, "\nTo enable Object Lock, run:\n") + fmt.Fprintf(writer, " s3.bucket.lock -name %s -enable\n", *bucketName) + } + + return nil + }) + + return err +}