Files
cephrgw-operator/internal/controller/objectstoreuser_controller.go
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

143 lines
4.6 KiB
Go

package controller
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"git.unkin.net/unkin/cephrgw-operator/api/v1alpha1"
"git.unkin.net/unkin/cephrgw-operator/internal/ceph"
)
// ObjectStoreUserReconciler provisions RGW users and delivers their keys.
type ObjectStoreUserReconciler struct {
client.Client
Scheme *runtime.Scheme
Ceph *ceph.Client
Endpoint string
}
// +kubebuilder:rbac:groups=ceph.unkin.net,resources=objectstoreusers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ceph.unkin.net,resources=objectstoreusers/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ceph.unkin.net,resources=objectstoreusers/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
func (r *ObjectStoreUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
var osu v1alpha1.ObjectStoreUser
if err := r.Get(ctx, req.NamespacedName, &osu); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
uid := orDefault(osu.Spec.UID, osu.Name)
secretName := orDefault(osu.Spec.SecretName, osu.Name+"-rgw")
if !osu.DeletionTimestamp.IsZero() {
if controllerutil.ContainsFinalizer(&osu, finalizer) {
if osu.Spec.RetainOnDelete {
logger.Info("retaining RGW user on delete", "uid", uid)
} else if err := r.Ceph.DeleteUser(ctx, uid); err != nil {
return r.fail(ctx, &osu, "DeleteFailed", err)
}
controllerutil.RemoveFinalizer(&osu, finalizer)
if err := r.Update(ctx, &osu); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
if controllerutil.AddFinalizer(&osu, finalizer) {
if err := r.Update(ctx, &osu); err != nil {
return ctrl.Result{}, err
}
}
spec := ceph.UserSpec{
UID: uid,
DisplayName: osu.Spec.DisplayName,
Email: osu.Spec.Email,
MaxBuckets: osu.Spec.MaxBuckets,
Suspended: osu.Spec.Suspended,
}
// Record adoption once: whether the RGW user already existed the first time
// we reconciled this resource (taken over rather than created). status.UID is
// only set on a successful reconcile, so it is a clean "never provisioned"
// signal that transient failures do not pollute.
firstObserve := osu.Status.UID == ""
_, getErr := r.Ceph.GetUser(ctx, uid)
switch {
case ceph.IsNotFound(getErr):
if _, err := r.Ceph.CreateUser(ctx, spec); err != nil {
return r.fail(ctx, &osu, "CreateFailed", err)
}
logger.Info("created RGW user", "uid", uid)
if firstObserve {
osu.Status.Adopted = false
}
case getErr != nil:
return r.fail(ctx, &osu, "LookupFailed", getErr)
default:
if _, err := r.Ceph.UpdateUser(ctx, spec); err != nil {
return r.fail(ctx, &osu, "UpdateFailed", err)
}
if firstObserve {
osu.Status.Adopted = true
logger.Info("adopted existing RGW user", "uid", uid)
}
}
if q := osu.Spec.Quota; q != nil {
if err := r.Ceph.SetUserQuota(ctx, uid, "user", q.Enabled, q.MaxSizeBytes, q.MaxObjects); err != nil {
return r.fail(ctx, &osu, "QuotaFailed", err)
}
}
user, err := r.Ceph.GetUser(ctx, uid)
if err != nil {
return r.fail(ctx, &osu, "LookupFailed", err)
}
key, ok := user.S3Key()
if !ok {
return r.fail(ctx, &osu, "NoKeys", fmt.Errorf("user %s has no S3 keys", uid))
}
if err := upsertSecret(ctx, r.Client, r.Scheme, &osu, secretName, osu.Namespace,
credentialSecretData(key, uid, r.Endpoint, "")); err != nil {
return r.fail(ctx, &osu, "SecretFailed", err)
}
osu.Status.Phase = "Ready"
osu.Status.UID = uid
osu.Status.SecretName = secretName
osu.Status.ObservedGeneration = osu.Generation
setReady(&osu.Status.Conditions, osu.Generation, true, "Provisioned", "RGW user provisioned")
if err := r.Status().Update(ctx, &osu); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: requeueSteady}, nil
}
func (r *ObjectStoreUserReconciler) fail(ctx context.Context, osu *v1alpha1.ObjectStoreUser, reason string, cause error) (ctrl.Result, error) {
osu.Status.Phase = "Error"
osu.Status.ObservedGeneration = osu.Generation
setReady(&osu.Status.Conditions, osu.Generation, false, reason, cause.Error())
if err := r.Status().Update(ctx, osu); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, cause
}
func (r *ObjectStoreUserReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.ObjectStoreUser{}).
Complete(r)
}