Support adopting existing radosgw buckets and users
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

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
This commit is contained in:
2026-07-25 00:15:10 +10:00
parent 619aa6751d
commit 54d3e38223
18 changed files with 578 additions and 50 deletions
+24 -13
View File
@@ -31,13 +31,15 @@ func (u *User) S3Key() (UserKey, bool) {
return u.Keys[0], true
}
// UserSpec describes the desired state of an RGW user.
// UserSpec describes the desired state of an RGW user. A nil DisplayName/
// Suspended (empty string / nil pointer) leaves that attribute untouched on an
// existing user, so an adopted user is not mutated unless the fields are set.
type UserSpec struct {
UID string
DisplayName string
Email string
MaxBuckets *int32
Suspended bool
Suspended *bool
}
// fromAdminUser converts a go-ceph admin.User into the subset the operator uses.
@@ -73,7 +75,7 @@ func (c *Client) CreateUser(ctx context.Context, spec UserSpec) (*User, error) {
DisplayName: firstNonEmpty(spec.DisplayName, spec.UID),
Email: spec.Email,
MaxBuckets: int32PtrToIntPtr(spec.MaxBuckets),
Suspended: boolToIntPtr(spec.Suspended),
Suspended: boolPtrToIntPtr(spec.Suspended),
GenerateKey: boolPtr(true),
})
if err != nil {
@@ -83,15 +85,20 @@ func (c *Client) CreateUser(ctx context.Context, spec UserSpec) (*User, error) {
return fromAdminUser(u), nil
}
// UpdateUser reconciles the mutable attributes of an existing RGW user.
// UpdateUser reconciles the mutable attributes of an existing RGW user. It only
// sends attributes the spec sets: an empty DisplayName or nil Suspended is left
// as-is, so reconciling (or adopting) a user does not clobber those fields.
func (c *Client) UpdateUser(ctx context.Context, spec UserSpec) (*User, error) {
u, err := c.admin.ModifyUser(ctx, admin.User{
ID: spec.UID,
DisplayName: firstNonEmpty(spec.DisplayName, spec.UID),
Email: spec.Email,
MaxBuckets: int32PtrToIntPtr(spec.MaxBuckets),
Suspended: boolToIntPtr(spec.Suspended),
})
req := admin.User{
ID: spec.UID,
Email: spec.Email,
MaxBuckets: int32PtrToIntPtr(spec.MaxBuckets),
Suspended: boolPtrToIntPtr(spec.Suspended),
}
if spec.DisplayName != "" {
req.DisplayName = spec.DisplayName
}
u, err := c.admin.ModifyUser(ctx, req)
if err != nil {
return nil, err
}
@@ -156,9 +163,13 @@ func int32PtrToIntPtr(p *int32) *int {
return &v
}
func boolToIntPtr(b bool) *int {
// boolPtrToIntPtr renders an optional bool as RGW's 0/1 int, or nil to omit.
func boolPtrToIntPtr(b *bool) *int {
if b == nil {
return nil
}
v := 0
if b {
if *b {
v = 1
}
return &v