Files
cephrgw-operator/internal/ceph/buckets.go
T
unkinben 54d3e38223
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Support adopting existing radosgw buckets and users
The operator previously assumed it created every user and bucket it managed:
reconciling an existing resource could overwrite its user attributes or wipe its
bucket policy, and deleting a CRD always deleted the underlying RGW object (only
Bucket had retainOnDelete). That made taking over pre-existing radosgw state
unsafe. Make adoption first-class.

- add retainOnDelete to ObjectStoreUser and BucketAccess (dedicated users), so
  deleting the CRD orphans the RGW user instead of deleting it (symmetric with
  Bucket)
- merge bucket policy instead of replacing it: the operator marks its own
  statements with a cephrgwop* Sid and preserves any statement it does not own,
  so adopting a bucket with a hand-written policy keeps it; add Bucket
  managePolicy (default true) to opt out of policy management entirely
- only reconcile user attributes the spec sets: DisplayName when non-empty and
  Suspended is now an optional *bool, so adopting a user does not reset them
- record adoption: ObjectStoreUser/Bucket status.adopted (+ printcolumn) is true
  when the RGW object already existed on first reconcile
- add GetBucketPolicy + MergeBucketPolicy; keyed adoption detection off the
  status identity field so a Pending owner wait does not mislabel it
- regenerate CRDs/deepcopy; add docs/adoption.md and
  config/samples/05-adoption.yaml; cover the merge in policy_test.go

Claude-Session: https://claude.ai/code/session_016CEncETbf8cvy1PhsHfFHM
2026-07-25 00:15:10 +10:00

223 lines
6.7 KiB
Go

package ceph
import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/ceph/go-ceph/rgw/admin"
)
// BucketInfo is the subset of an RGW bucket record the operator consumes, as
// returned by the Admin Ops API (GET /admin/bucket).
type BucketInfo struct {
Bucket string
Bid string
ID string
Owner string
}
// Name returns the bucket name regardless of the field radosgw used.
func (b *BucketInfo) Name() string {
if b.Bucket != "" {
return b.Bucket
}
return b.Bid
}
// InstanceID returns the RGW bucket instance id.
func (b *BucketInfo) InstanceID() string { return b.ID }
// CreateBucketSpec describes a bucket to create.
type CreateBucketSpec struct {
Bucket string
OwnerUID string
Zonegroup string
PlacementTarget string
LockEnabled bool
LockMode string
LockDays *int32
LockYears *int32
}
// GetBucket fetches a bucket by name via the Admin Ops API, returning an error
// classified by IsNotFound (admin.ErrNoSuchBucket) when it does not exist.
func (c *Client) GetBucket(ctx context.Context, name string) (*BucketInfo, error) {
b, err := c.admin.GetBucketInfo(ctx, admin.Bucket{Bucket: name})
if err != nil {
return nil, err
}
return &BucketInfo{Bucket: b.Bucket, ID: b.ID, Owner: b.Owner}, nil
}
// CreateBucket provisions a bucket owned by spec.OwnerUID. The Admin Ops API
// cannot create buckets, so the operator issues an S3 CreateBucket signed as the
// owner (which makes the owner the bucket owner directly).
func (c *Client) CreateBucket(ctx context.Context, spec CreateBucketSpec) (*BucketInfo, error) {
owner, err := c.asOwner(ctx, spec.OwnerUID)
if err != nil {
return nil, err
}
input := &s3.CreateBucketInput{Bucket: aws.String(spec.Bucket)}
if loc := locationConstraint(spec.Zonegroup, spec.PlacementTarget); loc != "" {
input.CreateBucketConfiguration = &s3types.CreateBucketConfiguration{
LocationConstraint: s3types.BucketLocationConstraint(loc),
}
}
if spec.LockEnabled {
input.ObjectLockEnabledForBucket = aws.Bool(true)
}
if _, err := c.s3.CreateBucket(ctx, input, owner); err != nil && !IsConflict(err) {
return nil, err
}
// Apply a default object-lock retention when requested.
if spec.LockEnabled && spec.LockMode != "" && (spec.LockDays != nil || spec.LockYears != nil) {
if err := c.setObjectLockDefault(ctx, owner, spec); err != nil {
return nil, err
}
}
return c.GetBucket(ctx, spec.Bucket)
}
// SetBucketVersioning enables or suspends S3 versioning on a bucket. bucketID is
// unused (kept for call-site stability).
func (c *Client) SetBucketVersioning(ctx context.Context, name, bucketID, ownerUID string, enabled bool) error {
owner, err := c.asOwner(ctx, ownerUID)
if err != nil {
return err
}
status := s3types.BucketVersioningStatusSuspended
if enabled {
status = s3types.BucketVersioningStatusEnabled
}
_, err = c.s3.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
Bucket: aws.String(name),
VersioningConfiguration: &s3types.VersioningConfiguration{Status: status},
}, owner)
return err
}
// SetBucketPolicy replaces the S3 bucket policy. An empty policy clears it.
// bucketID is unused (kept for call-site stability).
func (c *Client) SetBucketPolicy(ctx context.Context, name, bucketID, ownerUID, policy string) error {
owner, err := c.asOwner(ctx, ownerUID)
if err != nil {
return err
}
if policy == "" {
_, err := c.s3.DeleteBucketPolicy(ctx, &s3.DeleteBucketPolicyInput{Bucket: aws.String(name)}, owner)
if IsNotFound(err) {
return nil
}
return err
}
_, err = c.s3.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
Bucket: aws.String(name),
Policy: aws.String(policy),
}, owner)
return err
}
// GetBucketPolicy returns the bucket's current S3 policy JSON, or "" when it has
// none. It is signed as the bucket owner.
func (c *Client) GetBucketPolicy(ctx context.Context, name, ownerUID string) (string, error) {
owner, err := c.asOwner(ctx, ownerUID)
if err != nil {
return "", err
}
out, err := c.s3.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{Bucket: aws.String(name)}, owner)
if err != nil {
if IsNotFound(err) {
return "", nil
}
return "", err
}
if out.Policy == nil {
return "", nil
}
return *out.Policy, nil
}
// SetBucketTags replaces the bucket tag set. tagsJSON is the JSON produced by
// BuildTagJSON (a list of {"Key","Value"} objects). bucketID is unused (kept for
// call-site stability).
func (c *Client) SetBucketTags(ctx context.Context, name, bucketID, ownerUID, tagsJSON string) error {
owner, err := c.asOwner(ctx, ownerUID)
if err != nil {
return err
}
type tag struct {
Key string `json:"Key"`
Value string `json:"Value"`
}
var tags []tag
if tagsJSON != "" {
if err := json.Unmarshal([]byte(tagsJSON), &tags); err != nil {
return fmt.Errorf("ceph: parse bucket tags: %w", err)
}
}
if len(tags) == 0 {
_, err := c.s3.DeleteBucketTagging(ctx, &s3.DeleteBucketTaggingInput{Bucket: aws.String(name)}, owner)
if IsNotFound(err) {
return nil
}
return err
}
tagSet := make([]s3types.Tag, 0, len(tags))
for _, t := range tags {
tagSet = append(tagSet, s3types.Tag{Key: aws.String(t.Key), Value: aws.String(t.Value)})
}
_, err = c.s3.PutBucketTagging(ctx, &s3.PutBucketTaggingInput{
Bucket: aws.String(name),
Tagging: &s3types.Tagging{TagSet: tagSet},
}, owner)
return err
}
// DeleteBucket removes a bucket via the Admin Ops API. When purge is true its
// objects are deleted too. A NoSuchBucket response is treated as success.
func (c *Client) DeleteBucket(ctx context.Context, name string, purge bool) error {
err := c.admin.RemoveBucket(ctx, admin.Bucket{Bucket: name, PurgeObject: &purge})
if IsNotFound(err) {
return nil
}
return err
}
// setObjectLockDefault sets the bucket's default object-lock retention.
func (c *Client) setObjectLockDefault(ctx context.Context, owner func(*s3.Options), spec CreateBucketSpec) error {
_, err := c.s3.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
Bucket: aws.String(spec.Bucket),
ObjectLockConfiguration: &s3types.ObjectLockConfiguration{
ObjectLockEnabled: s3types.ObjectLockEnabledEnabled,
Rule: &s3types.ObjectLockRule{
DefaultRetention: &s3types.DefaultRetention{
Mode: s3types.ObjectLockRetentionMode(spec.LockMode),
Days: spec.LockDays,
Years: spec.LockYears,
},
},
},
}, owner)
return err
}
// locationConstraint renders the RGW LocationConstraint from a zonegroup and
// placement target ("<zonegroup>:<placement>"), or "" for default placement.
func locationConstraint(zonegroup, placement string) string {
loc := zonegroup
if placement != "" {
loc = zonegroup + ":" + placement
}
return loc
}