54d3e38223
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
177 lines
4.4 KiB
Go
177 lines
4.4 KiB
Go
package ceph
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/ceph/go-ceph/rgw/admin"
|
|
)
|
|
|
|
// UserKey is an S3 access/secret key pair belonging to an RGW user.
|
|
type UserKey struct {
|
|
User string
|
|
AccessKey string
|
|
SecretKey string
|
|
}
|
|
|
|
// User is the subset of an RGW user record the operator consumes.
|
|
type User struct {
|
|
UID string
|
|
DisplayName string
|
|
Email string
|
|
MaxBuckets int
|
|
Suspended int
|
|
Keys []UserKey
|
|
}
|
|
|
|
// S3Key returns the first access/secret key pair, if any.
|
|
func (u *User) S3Key() (UserKey, bool) {
|
|
if len(u.Keys) == 0 {
|
|
return UserKey{}, false
|
|
}
|
|
return u.Keys[0], true
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// fromAdminUser converts a go-ceph admin.User into the subset the operator uses.
|
|
func fromAdminUser(u admin.User) *User {
|
|
out := &User{
|
|
UID: u.ID,
|
|
DisplayName: u.DisplayName,
|
|
Email: u.Email,
|
|
MaxBuckets: derefInt(u.MaxBuckets),
|
|
Suspended: derefInt(u.Suspended),
|
|
}
|
|
for _, k := range u.Keys {
|
|
out.Keys = append(out.Keys, UserKey{User: k.User, AccessKey: k.AccessKey, SecretKey: k.SecretKey})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GetUser fetches an RGW user by uid, returning an error classified by
|
|
// IsNotFound (admin.ErrNoSuchUser) when it does not exist.
|
|
func (c *Client) GetUser(ctx context.Context, uid string) (*User, error) {
|
|
u, err := c.admin.GetUser(ctx, admin.User{ID: uid})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return fromAdminUser(u), nil
|
|
}
|
|
|
|
// CreateUser creates an RGW user, asking radosgw to generate an S3 key pair. The
|
|
// returned User carries the generated keys.
|
|
func (c *Client) CreateUser(ctx context.Context, spec UserSpec) (*User, error) {
|
|
u, err := c.admin.CreateUser(ctx, admin.User{
|
|
ID: spec.UID,
|
|
DisplayName: firstNonEmpty(spec.DisplayName, spec.UID),
|
|
Email: spec.Email,
|
|
MaxBuckets: int32PtrToIntPtr(spec.MaxBuckets),
|
|
Suspended: boolPtrToIntPtr(spec.Suspended),
|
|
GenerateKey: boolPtr(true),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.forgetIdentity(spec.UID)
|
|
return fromAdminUser(u), nil
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
}
|
|
return fromAdminUser(u), nil
|
|
}
|
|
|
|
// DeleteUser removes an RGW user. A NoSuchUser response is treated as success.
|
|
func (c *Client) DeleteUser(ctx context.Context, uid string) error {
|
|
err := c.admin.RemoveUser(ctx, admin.User{ID: uid})
|
|
c.forgetIdentity(uid)
|
|
if IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// SetUserQuota applies a quota to a user. quotaType is "user" or "bucket" (the
|
|
// latter sets the per-bucket default for buckets the user owns). A nil or
|
|
// negative limit means unlimited for that dimension.
|
|
func (c *Client) SetUserQuota(ctx context.Context, uid, quotaType string, enabled bool, maxSizeBytes, maxObjects *int64) error {
|
|
maxSize := valueOr(maxSizeBytes, -1)
|
|
maxObj := valueOr(maxObjects, -1)
|
|
return c.admin.SetUserQuota(ctx, admin.QuotaSpec{
|
|
UID: uid,
|
|
QuotaType: quotaType,
|
|
Enabled: &enabled,
|
|
MaxSize: &maxSize,
|
|
MaxObjects: &maxObj,
|
|
})
|
|
}
|
|
|
|
func firstNonEmpty(vals ...string) string {
|
|
for _, v := range vals {
|
|
if v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func valueOr(v *int64, fallback int64) int64 {
|
|
if v == nil || *v < 0 {
|
|
return fallback
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func derefInt(p *int) int {
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return *p
|
|
}
|
|
|
|
func boolPtr(b bool) *bool { return &b }
|
|
|
|
func int32PtrToIntPtr(p *int32) *int {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
v := int(*p)
|
|
return &v
|
|
}
|
|
|
|
// 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 {
|
|
v = 1
|
|
}
|
|
return &v
|
|
}
|