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
This commit is contained in:
@@ -64,6 +64,16 @@ type BucketSpec struct {
|
||||
// +optional
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
|
||||
// ManagePolicy controls whether the operator manages the bucket's S3 policy
|
||||
// from BucketAccess grants. When true (the default) the operator reconciles
|
||||
// its own statements while preserving any statements it does not own, so it
|
||||
// is safe to adopt a bucket that already has a policy. Set to false to leave
|
||||
// the bucket policy entirely untouched (BucketAccess grants then have no
|
||||
// effect on this bucket).
|
||||
// +kubebuilder:default=true
|
||||
// +optional
|
||||
ManagePolicy *bool `json:"managePolicy,omitempty"`
|
||||
|
||||
// RetainOnDelete keeps the RGW bucket (and its objects) when the Bucket
|
||||
// resource is deleted. By default the operator removes the empty bucket;
|
||||
// it never purges objects unless PurgeOnDelete is also set.
|
||||
@@ -94,6 +104,10 @@ type BucketStatus struct {
|
||||
// BucketAccess and reflected in the bucket policy.
|
||||
// +optional
|
||||
PolicyPrincipals int32 `json:"policyPrincipals,omitempty"`
|
||||
// Adopted reports that the RGW bucket already existed when the operator
|
||||
// first reconciled this resource (it was taken over, not created).
|
||||
// +optional
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// +optional
|
||||
@@ -108,6 +122,7 @@ type BucketStatus struct {
|
||||
// +kubebuilder:printcolumn:name="Bucket",type=string,JSONPath=`.status.bucketName`
|
||||
// +kubebuilder:printcolumn:name="Owner",type=string,JSONPath=`.status.owner`
|
||||
// +kubebuilder:printcolumn:name="Grants",type=integer,JSONPath=`.status.policyPrincipals`
|
||||
// +kubebuilder:printcolumn:name="Adopted",type=boolean,JSONPath=`.status.adopted`
|
||||
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||
|
||||
// Bucket is a Ceph RGW S3 bucket.
|
||||
|
||||
@@ -46,6 +46,12 @@ type BucketAccessSpec struct {
|
||||
// +optional
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
|
||||
// RetainOnDelete keeps the dedicated RGW user (created when UserRef is empty)
|
||||
// instead of deleting it when this BucketAccess is removed. Ignored when
|
||||
// UserRef is set (that user is never managed here). Defaults to false.
|
||||
// +optional
|
||||
RetainOnDelete bool `json:"retainOnDelete,omitempty"`
|
||||
|
||||
// Paths optionally scopes object-level access to these key prefixes within
|
||||
// the bucket; each becomes the resource "<bucket>/<prefix>*". Empty grants
|
||||
// the whole bucket. The bucket-level ListBucket action always applies to the
|
||||
|
||||
@@ -27,9 +27,11 @@ type ObjectStoreUserSpec struct {
|
||||
// +optional
|
||||
MaxBuckets *int32 `json:"maxBuckets,omitempty"`
|
||||
|
||||
// Suspended, when true, suspends the user so its keys stop working.
|
||||
// Suspended manages the user's suspended state: true suspends the user so
|
||||
// its keys stop working, false resumes it. When unset the operator does not
|
||||
// touch the suspended state (useful when adopting an existing user).
|
||||
// +optional
|
||||
Suspended bool `json:"suspended,omitempty"`
|
||||
Suspended *bool `json:"suspended,omitempty"`
|
||||
|
||||
// Quota optionally applies a user-level quota.
|
||||
// +optional
|
||||
@@ -40,6 +42,12 @@ type ObjectStoreUserSpec struct {
|
||||
// AWS_SECRET_ACCESS_KEY, BUCKET_HOST and the RGW uid.
|
||||
// +optional
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
|
||||
// RetainOnDelete keeps the RGW user (and its keys) when the ObjectStoreUser
|
||||
// resource is deleted, instead of removing it. Set this before adopting an
|
||||
// existing user you may later want to hand back. Defaults to false.
|
||||
// +optional
|
||||
RetainOnDelete bool `json:"retainOnDelete,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectStoreUserStatus reports observed user state.
|
||||
@@ -53,6 +61,10 @@ type ObjectStoreUserStatus struct {
|
||||
// SecretName is the Secret holding the user's credentials.
|
||||
// +optional
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
// Adopted reports that the RGW user already existed when the operator first
|
||||
// reconciled this resource (it was taken over, not created).
|
||||
// +optional
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
// +optional
|
||||
@@ -66,6 +78,7 @@ type ObjectStoreUserStatus struct {
|
||||
// +kubebuilder:resource:shortName=osu
|
||||
// +kubebuilder:printcolumn:name="UID",type=string,JSONPath=`.status.uid`
|
||||
// +kubebuilder:printcolumn:name="Secret",type=string,JSONPath=`.status.secretName`
|
||||
// +kubebuilder:printcolumn:name="Adopted",type=boolean,JSONPath=`.status.adopted`
|
||||
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
|
||||
|
||||
// ObjectStoreUser is a Ceph RGW S3 user whose keys are delivered into a Secret.
|
||||
|
||||
@@ -226,6 +226,11 @@ func (in *BucketSpec) DeepCopyInto(out *BucketSpec) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.ManagePolicy != nil {
|
||||
in, out := &in.ManagePolicy, &out.ManagePolicy
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BucketSpec.
|
||||
@@ -352,6 +357,11 @@ func (in *ObjectStoreUserSpec) DeepCopyInto(out *ObjectStoreUserSpec) {
|
||||
*out = new(int32)
|
||||
**out = **in
|
||||
}
|
||||
if in.Suspended != nil {
|
||||
in, out := &in.Suspended, &out.Suspended
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.Quota != nil {
|
||||
in, out := &in.Quota, &out.Quota
|
||||
*out = new(Quota)
|
||||
|
||||
Reference in New Issue
Block a user