Files
my-rook-config/pkg/operator/ceph/object/account/controller.go
T
parth-gr d6a0e9fd87 rgw: force delete the rgw accounts by looking at the force annotation
if the account cr has a force delete annotation, forcefully
remove the account, even if it contatins data in it, using the
purge data flag

Signed-off-by: parth-gr <partharora1010@gmail.com>
2026-07-29 13:03:06 +05:30

693 lines
31 KiB
Go

/*
Copyright 2026 The Rook Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package account manages RGW accounts.
package account
import (
"context"
"fmt"
"reflect"
"strconv"
"github.com/ceph/go-ceph/rgw/admin"
"github.com/coreos/pkg/capnslog"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/events"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/rook/rook/pkg/operator/ceph/object"
"github.com/rook/rook/pkg/operator/ceph/reporting"
"github.com/rook/rook/pkg/operator/k8sutil"
"github.com/rook/rook/pkg/util/log"
"k8s.io/apimachinery/pkg/util/validation"
)
const (
controllerName = "ceph-object-store-account-controller"
rgwAccountNameMaxLength = 64
)
// newMultisiteAdminOpsCtxFunc helps us mocking the admin ops API client in unit test
var newMultisiteAdminOpsCtxFunc = object.NewMultisiteAdminOpsContext
var logger = capnslog.NewPackageLogger("github.com/rook/rook", controllerName)
// Sets the type meta for the controller main object
var controllerTypeMeta = metav1.TypeMeta{
Kind: reflect.TypeFor[cephv1.CephObjectStoreAccount]().Name(),
APIVersion: fmt.Sprintf("%s/%s", cephv1.CustomResourceGroup, cephv1.Version),
}
// ReconcileObjectStoreAccount reconciles a CephObjectStoreAccount object
type ReconcileObjectStoreAccount struct {
client client.Client
scheme *runtime.Scheme
context *clusterd.Context
objContext *object.AdminOpsContext
cephClusterSpec *cephv1.ClusterSpec
clusterInfo *cephclient.ClusterInfo
opManagerContext context.Context
recorder events.EventRecorder
}
// Add creates a new CephObjectStoreAccount Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
// +kubebuilder:rbac:groups=ceph.rook.io,resources=cephobjectstoreaccounts,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ceph.rook.io,resources=cephobjectstoreaccounts/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ceph.rook.io,resources=cephobjectstoreaccounts/finalizers,verbs=update
func Add(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context, opConfig opcontroller.OperatorConfig) error {
return add(mgr, newReconciler(mgr, context, opManagerContext))
}
// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context) reconcile.Reconciler {
return &ReconcileObjectStoreAccount{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
context: context,
opManagerContext: opManagerContext,
recorder: mgr.GetEventRecorder("rook-" + controllerName),
}
}
func add(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
logger.Info("successfully started")
// Watch for changes on the CephObjectStoreAccount CRD object
err = c.Watch(
source.Kind(
mgr.GetCache(),
&cephv1.CephObjectStoreAccount{TypeMeta: controllerTypeMeta},
&handler.TypedEnqueueRequestForObject[*cephv1.CephObjectStoreAccount]{},
opcontroller.WatchControllerPredicate[*cephv1.CephObjectStoreAccount](mgr.GetScheme()),
),
)
if err != nil {
return err
}
return nil
}
// Reconcile reads that state of the cluster for a CephObjectStoreAccount object and makes changes based on the state read
// and what is in the CephObjectStoreAccount.Spec
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (r *ReconcileObjectStoreAccount) Reconcile(context context.Context, request reconcile.Request) (reconcile.Result, error) {
defer opcontroller.RecoverAndLogException()
// workaround because the rook logging mechanism is not compatible with the controller-runtime logging interface
reconcileResponse, cephObjectStoreAccount, err := r.reconcile(request)
if err != nil {
r.updateStatus(k8sutil.ObservedGenerationNotAvailable, request.NamespacedName, k8sutil.ReconcileFailedStatus)
log.NamedError(request.NamespacedName, logger, "failed to reconcile %v", err)
}
return reporting.ReportReconcileResult(logger, r.recorder, request, &cephObjectStoreAccount, reconcileResponse, err)
}
func (r *ReconcileObjectStoreAccount) reconcile(request reconcile.Request) (reconcile.Result, cephv1.CephObjectStoreAccount, error) {
// Fetch the CephObjectStoreAccount instance
cephObjectStoreAccount := &cephv1.CephObjectStoreAccount{}
err := r.client.Get(r.opManagerContext, request.NamespacedName, cephObjectStoreAccount)
if err != nil {
if kerrors.IsNotFound(err) {
log.NamedDebug(request.NamespacedName, logger, "CephObjectStoreAccount resource not found. Ignoring since object must be deleted.")
return reconcile.Result{}, *cephObjectStoreAccount, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrap(err, "failed to get CephObjectStoreAccount")
}
// update observedGeneration local variable with current generation value,
// because generation can be changed before reconcile got completed
// CR status will be updated at end of reconcile, so to reflect the reconcile has finished
observedGeneration := cephObjectStoreAccount.ObjectMeta.Generation
// Set a finalizer so we can do cleanup before the object goes away
generationUpdated, err := opcontroller.AddFinalizerIfNotPresent(r.opManagerContext, r.client, cephObjectStoreAccount)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrap(err, "failed to add finalizer")
}
if generationUpdated {
log.NamedInfo(request.NamespacedName, logger, "reconciling the object store account after adding finalizer")
return reconcile.Result{}, *cephObjectStoreAccount, nil
}
// The CR was just created, initializing status fields
if cephObjectStoreAccount.Status == nil {
r.updateStatus(k8sutil.ObservedGenerationNotAvailable, request.NamespacedName, k8sutil.EmptyStatus)
}
// Make sure a CephCluster is present otherwise do nothing
cephCluster, isReadyToReconcile, cephClusterExists, reconcileResponse := opcontroller.IsReadyToReconcile(r.opManagerContext, r.client, request.NamespacedName, controllerName)
if !isReadyToReconcile {
// This handles the case where the Ceph Cluster is gone and we want to delete that CR
// We skip the deleteAccount() function since everything is gone already
//
// Also, only remove the finalizer if the CephCluster is gone
// If not, we should wait for it to be ready
// This handles the case where the operator is not ready to accept Ceph command but the cluster exists
if !cephObjectStoreAccount.GetDeletionTimestamp().IsZero() && !cephClusterExists {
// Remove finalizer
err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, cephObjectStoreAccount)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrap(err, "failed to remove finalizer")
}
// Return and do not requeue. Successful deletion.
return reconcile.Result{}, *cephObjectStoreAccount, nil
}
return reconcileResponse, *cephObjectStoreAccount, nil
}
r.cephClusterSpec = &cephCluster.Spec
// Populate clusterInfo during each reconcile
r.clusterInfo, _, _, err = opcontroller.LoadClusterInfo(r.context, r.opManagerContext, request.NamespacedName.Namespace, r.cephClusterSpec)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrap(err, "failed to populate cluster info")
}
// Validate the object store has been initialized
opsCtx, objectStore, err := object.InitializeObjectStoreContext(r.context, r.clusterInfo, r.client, r.opManagerContext, cephObjectStoreAccount.Spec.Store, newMultisiteAdminOpsCtxFunc)
if err != nil {
if !cephObjectStoreAccount.GetDeletionTimestamp().IsZero() && kerrors.IsNotFound(err) {
log.NamedDebug(request.NamespacedName, logger, "deleting object store account")
r.recorder.Eventf(cephObjectStoreAccount, nil, corev1.EventTypeNormal, string(cephv1.ReconcileStarted), string(cephv1.ReconcileStarted), "deleting CephObjectStoreAccount %q", cephObjectStoreAccount.Name)
// Remove finalizer
err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, cephObjectStoreAccount)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrap(err, "failed to remove finalizer")
}
// Return and do not requeue. Successful deletion.
return reconcile.Result{}, *cephObjectStoreAccount, nil
}
log.NamedDebug(request.NamespacedName, logger, "ObjectStore resource not ready, retrying in %q. %v",
opcontroller.WaitForRequeueIfCephClusterNotReady.RequeueAfter.String(), err)
return opcontroller.WaitForRequeueIfCephClusterNotReady, *cephObjectStoreAccount, err
}
r.objContext = opsCtx
// DELETE: the CR was deleted
if !cephObjectStoreAccount.GetDeletionTimestamp().IsZero() {
log.NamedDebug(request.NamespacedName, logger, "deleting object store account")
r.recorder.Eventf(cephObjectStoreAccount, nil, corev1.EventTypeNormal, string(cephv1.ReconcileStarted), string(cephv1.ReconcileStarted), "deleting CephObjectStoreAccount %q", cephObjectStoreAccount.Name)
err := r.deleteAccount(cephObjectStoreAccount)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrapf(err, "failed to delete ceph object store account %q", cephObjectStoreAccount.Name)
}
// Remove finalizer
err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, cephObjectStoreAccount)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrap(err, "failed to remove finalizer")
}
// Return and do not requeue. Successful deletion.
return reconcile.Result{}, *cephObjectStoreAccount, nil
}
// CR is not deleted, continue reconciling
// Reconcile the account
accountID, err := r.reconcileAccount(cephObjectStoreAccount)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrapf(err, "failed to reconcile account %q", cephObjectStoreAccount.Name)
}
// Reconcile the root user
secretName, err := r.reconcileRootUser(cephObjectStoreAccount, accountID, objectStore)
if err != nil {
return reconcile.Result{}, *cephObjectStoreAccount, errors.Wrapf(err, "failed to reconcile root user")
}
// Update the status with the account ID and root user secret name
r.updateStatusWithAccountID(observedGeneration, request.NamespacedName, accountID, secretName)
return reconcile.Result{}, *cephObjectStoreAccount, nil
}
// getAccountName returns the effective account name from the CR spec,
// falling back to the CR name if spec.Name is not set.
func getAccountName(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) string {
if cephObjectStoreAccount.Spec.Name != "" {
return cephObjectStoreAccount.Spec.Name
}
return cephObjectStoreAccount.Name
}
// getAccountID looks up an already-persisted account ID from the CR,
// checking spec and status in priority order. It never generates
// a new ID. Returns "" when no ID has been recorded yet.
func getAccountID(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) string {
if cephObjectStoreAccount.Spec.AccountID != "" {
return cephObjectStoreAccount.Spec.AccountID
}
if cephObjectStoreAccount.Status != nil && cephObjectStoreAccount.Status.AccountID != "" {
return cephObjectStoreAccount.Status.AccountID
}
return ""
}
// getOrGenerateAccountID returns the existing account ID if one is persisted,
// otherwise derives a deterministic ID from the CR's UID. This is used during
// reconciliation to ensure the same ID is reused across retries, preventing
// orphaned accounts if the operator crashes after creating an account but
// before persisting the account ID to the CR status.
func getOrGenerateAccountID(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) (string, error) {
if id := getAccountID(cephObjectStoreAccount); id != "" {
return id, nil
}
if cephObjectStoreAccount.UID != "" {
nsName := types.NamespacedName{Namespace: cephObjectStoreAccount.Namespace, Name: cephObjectStoreAccount.Name}
log.NamedInfo(nsName, logger, "generating RGW account ID from the CR's UID")
id, err := generateDeterministicAccountID(cephObjectStoreAccount.UID)
if err != nil {
return "", errors.Wrap(err, "failed to generate deterministic account ID")
}
return id, nil
}
return "", nil
}
// generateDeterministicAccountID derives a stable RGW account ID from the
// CR's Kubernetes UID. It extracts 14 hex digits from the UUID, converts
// them to a 17-digit decimal number, and prefixes it with "RGW". The same
// UID always produces the same account ID.
func generateDeterministicAccountID(uid types.UID) (string, error) {
uidStr := string(uid)
l := len(uidStr)
if l < 14 {
return "", fmt.Errorf("UID too short to generate account ID: %q", uidStr)
}
last12 := uidStr[l-12:]
first2 := uidStr[:2]
value, err := strconv.ParseUint(last12+first2, 16, 64)
if err != nil {
return "", fmt.Errorf("failed to parse UID hex digits: %w", err)
}
return fmt.Sprintf("RGW%017d", value), nil
}
func (r *ReconcileObjectStoreAccount) reconcileAccount(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) (string, error) {
accountID, err := getOrGenerateAccountID(cephObjectStoreAccount)
if err != nil {
return "", errors.Wrap(err, "failed to get or generate account ID")
}
desiredAccount := admin.Account{
ID: accountID,
Name: getAccountName(cephObjectStoreAccount),
}
nsName := types.NamespacedName{Namespace: cephObjectStoreAccount.Namespace, Name: cephObjectStoreAccount.Name}
// Try to fetch the existing account
liveAccountExists := true
_, err = object.GetAccount(r.opManagerContext, r.objContext, desiredAccount.ID)
if err != nil {
if errors.Is(err, admin.ErrNoSuchKey) {
log.NamedInfo(nsName, logger, "account %q does not exist", desiredAccount.ID)
liveAccountExists = false
} else {
return "", errors.Wrapf(err, "failed to check if account exists")
}
}
// If Account exists then validate ownership and check if it needs to be updated
if liveAccountExists {
// Verify ownership before modifying an existing account. Ownership is confirmed if
// the status already records the account ID (from a previous reconcile or from
// the pre-creation bookmark persisted to status before account creation).
hasOwnershipProof := cephObjectStoreAccount.Status != nil && cephObjectStoreAccount.Status.AccountID == desiredAccount.ID
if !hasOwnershipProof {
return "", fmt.Errorf("account ID %q already exists in RGW but is not managed by this CR; refusing to adopt a foreign account", desiredAccount.ID)
}
log.NamedInfo(nsName, logger, "ensuring account %q is in sync", desiredAccount.ID)
updatedAccount, err := object.ModifyAccount(r.opManagerContext, r.objContext, desiredAccount)
if err != nil {
return "", errors.Wrapf(err, "failed to modify account %q", desiredAccount.ID)
}
log.NamedInfo(nsName, logger, "successfully modified account %q", desiredAccount.ID)
return updatedAccount.ID, nil
}
// Persist the account ID to status before creating the account. This serves as a
// creation bookmark so that if the operator crashes after creating the account but
// before the final status update, the next reconcile can identify the account as
// owned by this CR. The phase is not set to Ready here — that happens after
// successful creation.
if err := r.persistAccountIDToStatus(cephObjectStoreAccount, desiredAccount.ID); err != nil {
return "", errors.Wrapf(err, "failed to persist account ID to status before creating account %q", desiredAccount.Name)
}
// Account doesn't exist, create it
log.NamedInfo(nsName, logger, "creating account %q", desiredAccount.Name)
createdAccount, err := object.CreateAccount(r.opManagerContext, r.objContext, desiredAccount)
if err != nil {
if errors.Is(err, admin.ErrAccountAlreadyExists) {
return "", fmt.Errorf("failed to create account with ID %q and name %q: an account with this ID or name already exists in RGW; ensure no conflicting account exists", desiredAccount.ID, desiredAccount.Name)
}
return "", errors.Wrapf(err, "failed to create account %q", desiredAccount.Name)
}
log.NamedInfo(nsName, logger, "successfully created account %q with ID %q", desiredAccount.Name, createdAccount.ID)
return createdAccount.ID, nil
}
// persistAccountIDToStatus persists the account ID to the CR status before
// creating the account in the backend. This is a no-op if the status already
// contains the desired account ID. The phase is not modified here; it will be
// set to Ready after the account is successfully created.
func (r *ReconcileObjectStoreAccount) persistAccountIDToStatus(cephObjectStoreAccount *cephv1.CephObjectStoreAccount, accountID string) error {
if cephObjectStoreAccount.Status != nil && cephObjectStoreAccount.Status.AccountID == accountID {
return nil
}
// Re-fetch the latest version to avoid resourceVersion conflicts, since earlier
// reconcile steps (e.g. updateStatus for initial empty status) may have updated
// the object on the server, making our in-memory copy stale.
nsName := types.NamespacedName{Namespace: cephObjectStoreAccount.Namespace, Name: cephObjectStoreAccount.Name}
latest := &cephv1.CephObjectStoreAccount{}
if err := r.client.Get(r.opManagerContext, nsName, latest); err != nil {
return errors.Wrapf(err, "failed to get latest version of object %q", nsName)
}
if latest.Status == nil {
latest.Status = &cephv1.ObjectStoreAccountStatus{}
}
latest.Status.AccountID = accountID
if err := reporting.UpdateStatus(r.client, latest); err != nil {
return errors.Wrapf(err, "failed to update object %q status", nsName)
}
// Update the caller's copy so subsequent checks see the persisted account ID
cephObjectStoreAccount.Status = latest.Status
return nil
}
func (r *ReconcileObjectStoreAccount) deleteAccount(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) error {
nsName := types.NamespacedName{Namespace: cephObjectStoreAccount.Namespace, Name: cephObjectStoreAccount.Name}
accountID := getAccountID(cephObjectStoreAccount)
if accountID == "" {
log.NamedInfo(nsName, logger, "no account ID found, skipping deletion")
return nil
}
// Only delete the account if we have ownership proof (status contains the account ID).
// If the account ID is only in spec but was never successfully reconciled,
// it means the controller never created/adopted this account, so we must
// not delete what could be a foreign account.
hasOwnershipProof := cephObjectStoreAccount.Status != nil && cephObjectStoreAccount.Status.AccountID == accountID
if !hasOwnershipProof {
log.NamedInfo(nsName, logger, "account %q was never successfully managed by this CR, skipping deletion to avoid removing a foreign account", accountID)
return nil
}
// Always attempt to delete the root user to ensure cleanup
rootUserID := getRootUserID(cephObjectStoreAccount)
log.NamedInfo(nsName, logger, "deleting root user %q for account %q", rootUserID, accountID)
err := object.DeleteAccountRootUser(r.opManagerContext, r.objContext, rootUserID)
if err != nil {
if !errors.Is(err, admin.ErrNoSuchUser) {
return errors.Wrapf(err, "failed to delete root user %q for account %q", rootUserID, accountID)
}
log.NamedInfo(nsName, logger, "root user %q not found, considering deletion successful", rootUserID)
} else {
log.NamedInfo(nsName, logger, "successfully deleted root user %q", rootUserID)
}
log.NamedInfo(nsName, logger, "deleting account %q", accountID)
// if `rook.io/force-deletion` annotation is set to true, we will force delete the account even if it contains S3 resources
if opcontroller.ForceDeleteRequested(cephObjectStoreAccount.Annotations) {
log.NamedInfo(nsName, logger, "force-deletion annotation set to true, forcing deletion of account %q", accountID)
err = object.ForceDeleteAccount(nsName, r.objContext, accountID)
if err != nil {
return errors.Wrapf(err, "failed to force delete account %q", accountID)
}
} else {
log.NamedInfo(nsName, logger, "force-deletion annotation not set for account %q, manual cleanup may be required if the account contains S3 resources", accountID)
err = object.DeleteAccount(nsName, r.opManagerContext, r.objContext, accountID)
if err != nil {
return errors.Wrapf(err, "failed to delete account %q, manual cleanup may be required if the account contains S3 resources", accountID)
}
}
log.NamedInfo(nsName, logger, "successfully deleted account %q", accountID)
return nil
}
// deleteRootUserSecret deletes the Kubernetes secret for root user credentials, treating "not found" as success.
func (r *ReconcileObjectStoreAccount) deleteRootUserSecret(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) error {
nsName := types.NamespacedName{Namespace: cephObjectStoreAccount.Namespace, Name: cephObjectStoreAccount.Name}
secretName := generateRootUserSecretName(cephObjectStoreAccount)
log.NamedInfo(nsName, logger, "deleting root user secret %q", secretName)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: cephObjectStoreAccount.Namespace,
},
}
if err := r.client.Delete(r.opManagerContext, secret); err != nil {
if kerrors.IsNotFound(err) {
log.NamedInfo(nsName, logger, "root user secret %q not found, considering deletion successful", secretName)
} else {
return errors.Wrapf(err, "failed to delete root user secret %q", secretName)
}
} else {
log.NamedInfo(nsName, logger, "successfully deleted root user secret %q", secretName)
}
return nil
}
// skipRootUserCreation returns true if root user creation should be skipped.
func skipRootUserCreation(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) bool {
return cephObjectStoreAccount.Spec.RootUser != nil && cephObjectStoreAccount.Spec.RootUser.SkipCreate != nil && *cephObjectStoreAccount.Spec.RootUser.SkipCreate
}
// getRootUserID returns the root user UID derived from the CR's metadata.uid.
// The root user's UID is the metadata.uid of the CephObjectStoreAccount CR,
// ensuring global uniqueness across multi-cluster and multisite environments.
func getRootUserID(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) string {
return string(cephObjectStoreAccount.UID)
}
// getRootUserDisplayName returns the display name for the root user.
func getRootUserDisplayName(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) string {
if cephObjectStoreAccount.Spec.RootUser != nil && cephObjectStoreAccount.Spec.RootUser.DisplayName != "" {
return cephObjectStoreAccount.Spec.RootUser.DisplayName
}
// RGW enforces a maximum display name length of 64 characters
return truncate(fmt.Sprintf("root-%s", cephObjectStoreAccount.Name), rgwAccountNameMaxLength)
}
// accountResourceLabels returns the standard labels for resources related to a CephObjectStoreAccount.
func accountResourceLabels(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) map[string]string {
return map[string]string{
"cephObjectStoreAccountName": truncate(cephObjectStoreAccount.Name, validation.DNS1123LabelMaxLength),
"cephObjectStoreName": truncate(cephObjectStoreAccount.Spec.Store, validation.DNS1123LabelMaxLength),
"cephObjectStoreNamespace": truncate(cephObjectStoreAccount.Namespace, validation.DNS1123LabelMaxLength),
}
}
// truncate truncates a string to the specified maximum length.
func truncate(value string, maxLen int) string {
if len(value) > maxLen {
return value[:maxLen]
}
return value
}
// generateRootUserSecretName returns the name of the Kubernetes secret for root user credentials.
func generateRootUserSecretName(cephObjectStoreAccount *cephv1.CephObjectStoreAccount) string {
return fmt.Sprintf("rook-ceph-object-root-user-%s", cephObjectStoreAccount.Name)
}
// reconcileRootUser creates or updates the root user for the account and manages its credentials secret.
// Returns the secret name if a root user was created, or empty string if skipped.
func (r *ReconcileObjectStoreAccount) reconcileRootUser(cephObjectStoreAccount *cephv1.CephObjectStoreAccount, accountID string, objectStore *cephv1.CephObjectStore) (string, error) {
nsName := types.NamespacedName{Namespace: cephObjectStoreAccount.Namespace, Name: cephObjectStoreAccount.Name}
if skipRootUserCreation(cephObjectStoreAccount) {
// If skipCreate is set, delete any previously created root user
rootUserID := getRootUserID(cephObjectStoreAccount)
log.NamedInfo(nsName, logger, "skipCreate is true, deleting root user %q if it exists", rootUserID)
err := object.DeleteAccountRootUser(r.opManagerContext, r.objContext, rootUserID)
if err != nil && !errors.Is(err, admin.ErrNoSuchUser) {
return "", errors.Wrapf(err, "failed to delete root user %q for account %q", rootUserID, accountID)
}
log.NamedInfo(nsName, logger, "successfully deleted root user %q", rootUserID)
// Delete the stale credentials secret since the root user no longer exists
if err := r.deleteRootUserSecret(cephObjectStoreAccount); err != nil {
return "", err
}
return "", nil
}
rootUserID := getRootUserID(cephObjectStoreAccount)
displayName := getRootUserDisplayName(cephObjectStoreAccount)
accountRoot := true
generateKey := true
// Check if the root user already exists
user, err := object.GetAccountRootUser(r.opManagerContext, r.objContext, rootUserID)
if err != nil {
if !errors.Is(err, admin.ErrNoSuchUser) {
return "", errors.Wrapf(err, "failed to check if root user %q exists", rootUserID)
}
// Root user does not exist, create it
log.NamedInfo(nsName, logger, "creating root user %q for account %q", rootUserID, accountID)
user, err = object.CreateAccountRootUser(r.opManagerContext, r.objContext, admin.User{
ID: rootUserID,
DisplayName: displayName,
AccountID: accountID,
AccountRoot: &accountRoot,
GenerateKey: &generateKey,
})
if err != nil {
return "", errors.Wrapf(err, "failed to create root user %q for account %q", rootUserID, accountID)
}
log.NamedInfo(nsName, logger, "successfully created root user %q for account %q", rootUserID, accountID)
} else {
// Root user exists, always update to ensure desired state
log.NamedInfo(nsName, logger, "updating root user %q", rootUserID)
user, err = object.ModifyAccountRootUser(r.opManagerContext, r.objContext, admin.User{
ID: rootUserID,
DisplayName: displayName,
})
if err != nil {
return "", errors.Wrapf(err, "failed to update root user %q", rootUserID)
}
log.NamedInfo(nsName, logger, "successfully updated root user %q", rootUserID)
}
// Reconcile the credentials secret
secretName, err := r.reconcileRootUserSecret(cephObjectStoreAccount, &user, objectStore)
if err != nil {
return "", errors.Wrapf(err, "failed to reconcile root user secret")
}
return secretName, nil
}
// reconcileRootUserSecret creates or updates the Kubernetes secret containing root user credentials.
func (r *ReconcileObjectStoreAccount) reconcileRootUserSecret(cephObjectStoreAccount *cephv1.CephObjectStoreAccount, user *admin.User, objectStore *cephv1.CephObjectStore) (string, error) {
secretName := generateRootUserSecretName(cephObjectStoreAccount)
secrets := map[string]string{
"AccessKey": user.Keys[0].AccessKey,
"SecretKey": user.Keys[0].SecretKey,
"Endpoint": r.objContext.Endpoint,
}
if objectStore.Spec.Gateway.SSLCertificateRef != "" {
secrets["SSLCertSecretName"] = objectStore.Spec.Gateway.SSLCertificateRef
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: cephObjectStoreAccount.Namespace,
Labels: accountResourceLabels(cephObjectStoreAccount),
},
StringData: secrets,
Type: k8sutil.RookType,
}
// Set owner reference to the CephObjectStoreAccount CR
if err := controllerutil.SetControllerReference(cephObjectStoreAccount, secret, r.scheme); err != nil {
return "", errors.Wrapf(err, "failed to set owner reference of root user secret %q", secretName)
}
// Create or update the secret
if err := opcontroller.CreateOrUpdateObject(r.opManagerContext, r.client, secret); err != nil {
return "", errors.Wrapf(err, "failed to create or update root user secret %q", secretName)
}
return secretName, nil
}
func (r *ReconcileObjectStoreAccount) updateStatus(observedGeneration int64, name types.NamespacedName, status string) {
account := &cephv1.CephObjectStoreAccount{}
if err := r.client.Get(r.opManagerContext, name, account); err != nil {
if kerrors.IsNotFound(err) {
log.NamedDebug(name, logger, "CephObjectStoreAccount resource not found. Ignoring since object must be deleted.")
return
}
log.NamedWarning(name, logger, "failed to retrieve object store account %q to update status. %v", name, err)
return
}
if account.Status == nil {
account.Status = &cephv1.ObjectStoreAccountStatus{}
}
account.Status.Phase = status
if observedGeneration != k8sutil.ObservedGenerationNotAvailable {
account.Status.ObservedGeneration = &observedGeneration
}
if err := reporting.UpdateStatus(r.client, account); err != nil {
log.NamedError(name, logger, "failed to set object store account %q status to %q. %v", name, status, err)
return
}
log.NamedDebug(name, logger, "object store account %q status updated to %q", name, status)
}
func (r *ReconcileObjectStoreAccount) updateStatusWithAccountID(observedGeneration int64, name types.NamespacedName, accountID, rootAccountSecretName string) {
account := &cephv1.CephObjectStoreAccount{}
if err := r.client.Get(r.opManagerContext, name, account); err != nil {
if kerrors.IsNotFound(err) {
log.NamedDebug(name, logger, "CephObjectStoreAccount resource not found. Ignoring since object must be deleted.")
return
}
log.NamedWarning(name, logger, "failed to retrieve object store account %q to update status. %v", name, err)
return
}
if account.Status == nil {
account.Status = &cephv1.ObjectStoreAccountStatus{}
}
account.Status.Phase = k8sutil.ReadyStatus
account.Status.AccountID = accountID
account.Status.RootAccountSecretName = rootAccountSecretName
if observedGeneration != k8sutil.ObservedGenerationNotAvailable {
account.Status.ObservedGeneration = &observedGeneration
}
if err := reporting.UpdateStatus(r.client, account); err != nil {
log.NamedError(name, logger, "failed to update object store account %q status. %v", name, err)
return
}
log.NamedDebug(name, logger, "object store account %q status updated with account ID %q", name, accountID)
}