Files
seaweedfs/weed/shell/command_s3_bucket_create.go
T
Chris LuGitHubDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
b750853c42 shell: refuse s3.bucket.create on an existing bucket (#11455)
* shell: refuse s3.bucket.create on an existing bucket

CreateEntry without o_excl replaces the bucket entry, dropping every
extended attribute: lifecycle configuration, owner, versioning and the
irreversible Object Lock flag. Send o_excl so a re-run fails with
'bucket already exists' instead of silently resetting the bucket.

* filer: fail exclusive creates when the lookup itself fails

CreateEntry discards FindEntry errors, so an o_excl create hitting a
transient store failure would take the insert path and upsert over the
entry it was meant to preserve. Propagate the lookup error when o_excl
is set; non-exclusive creates keep their existing semantics.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* shell: test s3.bucket.create requests an exclusive create

Exercises the command end to end through a fake filer gRPC server and
asserts the OExcl flag reaches the wire along with the already-exists
error path.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* shell: synchronize captured requests and assert the exact bucket error

The fake filer records CreateEntry requests on the gRPC server goroutine,
so reads need the same mutex; the test also now checks for the exact
"bucket my-bucket already exists" message rather than any error that
mentions existence.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-25 22:04:54 +08:00

144 lines
3.9 KiB
Go

package shell
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3bucket"
)
func init() {
Commands = append(Commands, &commandS3BucketCreate{})
}
type commandS3BucketCreate struct {
}
func (c *commandS3BucketCreate) Name() string {
return "s3.bucket.create"
}
func (c *commandS3BucketCreate) Help() string {
return `create a bucket with a given name
Example:
s3.bucket.create -name <bucket_name>
s3.bucket.create -name <bucket_name> -owner <identity_name>
s3.bucket.create -name <bucket_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.
If not specified, the bucket will have no owner and will only be accessible
by admin users.
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.
`
}
func (c *commandS3BucketCreate) HasTag(CommandTag) bool {
return false
}
func (c *commandS3BucketCreate) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
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
}
if *bucketName == "" {
return fmt.Errorf("empty bucket name")
}
err = s3bucket.VerifyS3BucketName(*bucketName)
if err != nil {
return err
}
// Trim whitespace from owner and treat whitespace-only as empty
owner := strings.TrimSpace(*bucketOwner)
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
fmt.Fprintln(writer, "create bucket under", filerBucketsPath)
entry := &filer_pb.Entry{
Name: *bucketName,
IsDirectory: true,
Attributes: &filer_pb.FuseAttributes{
Mtime: time.Now().Unix(),
Crtime: time.Now().Unix(),
FileMode: uint32(0777 | os.ModeDir),
},
}
// Set bucket owner if specified
if owner != "" {
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
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)
}
createErr := filer_pb.CreateEntry(context.Background(), client, &filer_pb.CreateEntryRequest{
Directory: filerBucketsPath,
Entry: entry,
OExcl: true,
})
if errors.Is(createErr, filer_pb.ErrEntryAlreadyExists) {
return fmt.Errorf("bucket %s already exists", *bucketName)
}
if createErr != nil {
return createErr
}
fmt.Fprintln(writer, "created bucket", *bucketName)
if owner != "" {
fmt.Fprintln(writer, "bucket owner:", owner)
}
if *withLock {
fmt.Fprintln(writer, "Object Lock: enabled")
fmt.Fprintln(writer, "Versioning: enabled")
}
return nil
})
return err
}