diff --git a/Documentation/CRDs/specification.md b/Documentation/CRDs/specification.md index 7b7574682..54f0810d5 100644 --- a/Documentation/CRDs/specification.md +++ b/Documentation/CRDs/specification.md @@ -3388,6 +3388,18 @@ SnapshotScheduleStatusSpec (Optional) + + +conditions
+ + +[]Condition + + + + + +

CephBlockPoolStatus @@ -5149,7 +5161,7 @@ The default is not set.

Condition

-(Appears on:CephBlockPoolStatus, CephFilesystemStatus, ClusterStatus, ObjectStoreStatus, Status) +(Appears on:CephBlockPoolRadosNamespaceStatus, CephBlockPoolStatus, CephFilesystemStatus, ClusterStatus, ObjectStoreStatus, Status)

Condition represents a status condition on any Rook-Ceph Custom Resource.

@@ -5275,6 +5287,22 @@ deletion.

ObjectHasNoDependentsReason represents when a resource object has no dependents that are blocking deletion.

+

"PoolEmpty"

+

PoolEmptyReason represents when a pool does not contain images or snapshots that are blocking +deletion.

+ +

"PoolNotEmpty"

+

PoolNotEmptyReason represents when a pool contains images or snapshots that are blocking +deletion.

+ +

"RadosNamespaceEmpty"

+

RadosNamespaceEmptyReason represents when a rados namespace does not contain images or snapshots that are blocking +deletion.

+ +

"RadosNamespaceNotEmpty"

+

RadosNamespaceNotEmptyReason represents when a rados namespace contains images or snapshots that are blocking +deletion.

+

"ReconcileFailed"

ReconcileFailed represents when a resource reconciliation failed.

@@ -5316,9 +5344,15 @@ blocking deletion.

"Failure"

ConditionFailure represents Failure state of an object

+

"PoolDeletionIsBlocked"

+

ConditionPoolDeletionIsBlocked represents when deletion of the object is blocked.

+

"Progressing"

ConditionProgressing represents Progressing state of an object

+

"RadosNamespaceDeletionIsBlocked"

+

ConditionRadosNSDeletionIsBlocked represents when deletion of the object is blocked.

+

"Ready"

ConditionReady represents Ready state of an object

diff --git a/deploy/charts/rook-ceph/templates/resources.yaml b/deploy/charts/rook-ceph/templates/resources.yaml index 93e44f04d..3c53d41cc 100644 --- a/deploy/charts/rook-ceph/templates/resources.yaml +++ b/deploy/charts/rook-ceph/templates/resources.yaml @@ -104,6 +104,28 @@ spec: status: description: Status represents the status of a CephBlockPool Rados Namespace properties: + conditions: + items: + description: Condition represents a status condition on any Rook-Ceph Custom Resource. + properties: + lastHeartbeatTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + description: ConditionReason is a reason for a condition + type: string + status: + type: string + type: + description: ConditionType represent a resource's status + type: string + type: object + type: array info: additionalProperties: type: string diff --git a/deploy/examples/crds.yaml b/deploy/examples/crds.yaml index 516afd049..d59a19ca0 100644 --- a/deploy/examples/crds.yaml +++ b/deploy/examples/crds.yaml @@ -107,6 +107,28 @@ spec: status: description: Status represents the status of a CephBlockPool Rados Namespace properties: + conditions: + items: + description: Condition represents a status condition on any Rook-Ceph Custom Resource. + properties: + lastHeartbeatTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + description: ConditionReason is a reason for a condition + type: string + status: + type: string + type: + description: ConditionType represent a resource's status + type: string + type: object + type: array info: additionalProperties: type: string diff --git a/pkg/apis/ceph.rook.io/v1/namespace.go b/pkg/apis/ceph.rook.io/v1/namespace.go new file mode 100644 index 000000000..58b4b3e45 --- /dev/null +++ b/pkg/apis/ceph.rook.io/v1/namespace.go @@ -0,0 +1,31 @@ +/* +Copyright 2025 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 v1 + +const ( + ImplicitNamespaceKey = "" + ImplicitNamespaceVal = "" +) + +func GetRadosNamespaceName(cephBlockPoolRadosNamespace *CephBlockPoolRadosNamespace) string { + if cephBlockPoolRadosNamespace.Spec.Name == ImplicitNamespaceKey { + return ImplicitNamespaceVal + } else if cephBlockPoolRadosNamespace.Spec.Name != "" { + return cephBlockPoolRadosNamespace.Spec.Name + } + return cephBlockPoolRadosNamespace.Name +} diff --git a/pkg/apis/ceph.rook.io/v1/pool.go b/pkg/apis/ceph.rook.io/v1/pool.go index 14fa7afe3..2ebe92374 100644 --- a/pkg/apis/ceph.rook.io/v1/pool.go +++ b/pkg/apis/ceph.rook.io/v1/pool.go @@ -95,6 +95,10 @@ func (p *CephBlockPool) GetStatusConditions() *[]Condition { return &p.Status.Conditions } +func (p *CephBlockPoolRadosNamespace) GetStatusConditions() *[]Condition { + return &p.Status.Conditions +} + // SnapshotSchedulesEnabled returns whether snapshot schedules are desired func (p *MirroringSpec) SnapshotSchedulesEnabled() bool { return len(p.SnapshotSchedules) > 0 diff --git a/pkg/apis/ceph.rook.io/v1/types.go b/pkg/apis/ceph.rook.io/v1/types.go index a15f4e868..0bea94326 100755 --- a/pkg/apis/ceph.rook.io/v1/types.go +++ b/pkg/apis/ceph.rook.io/v1/types.go @@ -568,6 +568,18 @@ const ( // ObjectHasNoDependentsReason represents when a resource object has no dependents that are // blocking deletion. ObjectHasNoDependentsReason ConditionReason = "ObjectHasNoDependents" + // PoolNotEmptyReason represents when a pool contains images or snapshots that are blocking + // deletion. + PoolNotEmptyReason ConditionReason = "PoolNotEmpty" + // PoolEmptyReason represents when a pool does not contain images or snapshots that are blocking + // deletion. + PoolEmptyReason ConditionReason = "PoolEmpty" + // RadosNamespaceNotEmptyReason represents when a rados namespace contains images or snapshots that are blocking + // deletion. + RadosNamespaceNotEmptyReason ConditionReason = "RadosNamespaceNotEmpty" + // RadosNamespaceEmptyReason represents when a rados namespace does not contain images or snapshots that are blocking + // deletion. + RadosNamespaceEmptyReason ConditionReason = "RadosNamespaceEmpty" ) // ConditionType represent a resource's status @@ -589,6 +601,10 @@ const ( // ConditionDeletionIsBlocked represents when deletion of the object is blocked. ConditionDeletionIsBlocked ConditionType = "DeletionIsBlocked" + // ConditionPoolDeletionIsBlocked represents when deletion of the object is blocked. + ConditionPoolDeletionIsBlocked ConditionType = "PoolDeletionIsBlocked" + // ConditionRadosNSDeletionIsBlocked represents when deletion of the object is blocked. + ConditionRadosNSDeletionIsBlocked ConditionType = "RadosNamespaceDeletionIsBlocked" ) // ClusterState represents the state of a Ceph Cluster @@ -3534,6 +3550,7 @@ type CephBlockPoolRadosNamespaceStatus struct { MirroringInfo *MirroringInfoSpec `json:"mirroringInfo,omitempty"` // +optional SnapshotScheduleStatus *SnapshotScheduleStatusSpec `json:"snapshotScheduleStatus,omitempty"` + Conditions []Condition `json:"conditions,omitempty"` } // Represents the source of a volume to mount. diff --git a/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go b/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go index 5ad7a281d..987aa341b 100644 --- a/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go +++ b/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go @@ -502,6 +502,13 @@ func (in *CephBlockPoolRadosNamespaceStatus) DeepCopyInto(out *CephBlockPoolRado *out = new(SnapshotScheduleStatusSpec) (*in).DeepCopyInto(*out) } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/daemon/ceph/client/mirror.go b/pkg/daemon/ceph/client/mirror.go index d3c8e82f7..06eb6c895 100644 --- a/pkg/daemon/ceph/client/mirror.go +++ b/pkg/daemon/ceph/client/mirror.go @@ -53,10 +53,8 @@ type Images struct { } const ( - mirrorModeDisabled = "disabled" - mirrorModeInitOnly = "init-only" - ImplicitNamespaceKey = "" - ImplicitNamespaceVal = "" + mirrorModeDisabled = "disabled" + mirrorModeInitOnly = "init-only" ) var ( @@ -398,8 +396,8 @@ func EnableRBDRadosNamespaceMirroring(context *clusterd.Context, clusterInfo *Cl return errors.Errorf("ceph version %q does not support mirroring in rados namespace %q with --remote-namespace flag, supported version are v20 and above.", clusterInfo.CephVersion.String(), poolAndRadosNamespaceName) } - if remoteNamespace != nil && *remoteNamespace == ImplicitNamespaceKey { - *remoteNamespace = ImplicitNamespaceVal + if remoteNamespace != nil && *remoteNamespace == cephv1.ImplicitNamespaceKey { + *remoteNamespace = cephv1.ImplicitNamespaceVal } args := []string{"mirror", "pool", "enable", poolAndRadosNamespaceName, mode} diff --git a/pkg/daemon/ceph/client/pool.go b/pkg/daemon/ceph/client/pool.go index 9723056ea..c60fa5965 100644 --- a/pkg/daemon/ceph/client/pool.go +++ b/pkg/daemon/ceph/client/pool.go @@ -220,22 +220,54 @@ func CreatePoolWithPGs(context *clusterd.Context, clusterInfo *ClusterInfo, clus true /* enableECOverwrite */) } +func IsPoolEmpty(context *clusterd.Context, clusterInfo *ClusterInfo, name string, radosNamespaces []string) (bool, string, error) { + logger.Debugf("checking if pool %q in namespace %q is empty", name, clusterInfo.Namespace) + + isEmpty, err := checkIsPoolEmpty(context, clusterInfo, name) + if err != nil { + return false, fmt.Sprintf("failed to check if pool %q is empty", name), err + } + if !isEmpty { + return false, fmt.Sprintf("pool %q contains images or snapshots and cannot be deleted", name), nil + } + // Check the rados namespaces for images/snapshots + for _, radosNamespace := range radosNamespaces { + containsImages, err := checkForImagesInRadosNamespace(context, clusterInfo, name, radosNamespace) + if err != nil || containsImages { + return false, fmt.Sprintf("rados namespace %q contains images or snapshots preventing pool %q deletion", radosNamespace, name), nil + } + } + // No images/snapshots found in the pool or rados namespaces + return true, fmt.Sprintf("pool %q is empty and can be deleted", name), nil +} + func checkForImagesInPool(context *clusterd.Context, clusterInfo *ClusterInfo, name string) error { + isEmpty, err := checkIsPoolEmpty(context, clusterInfo, name) + if err != nil { + return err + } + if isEmpty { + return nil + } + return errors.Errorf("pool %q contains images/snapshots", name) +} + +func checkIsPoolEmpty(context *clusterd.Context, clusterInfo *ClusterInfo, name string) (bool, error) { var err error logger.Debugf("checking any images/snapshots present in pool %q", name) stats, err := GetPoolStatistics(context, clusterInfo, name) if err != nil { if strings.Contains(err.Error(), "No such file or directory") { - return nil + return false, nil } - return errors.Wrapf(err, "failed to list images/snapshots in pool %s", name) + return false, errors.Wrapf(err, "failed to list images/snapshots in pool %s", name) } if stats.Images.Count == 0 && stats.Images.SnapCount == 0 { logger.Infof("no images/snapshots present in pool %q", name) - return nil + return true, nil } - return errors.Errorf("pool %q contains images/snapshots", name) + return false, nil } // DeletePool purges a pool from Ceph diff --git a/pkg/daemon/ceph/client/radosnamespace.go b/pkg/daemon/ceph/client/radosnamespace.go index bb6b185a8..2d5c47864 100644 --- a/pkg/daemon/ceph/client/radosnamespace.go +++ b/pkg/daemon/ceph/client/radosnamespace.go @@ -69,25 +69,27 @@ func getRadosNamespaceStatistics(context *clusterd.Context, clusterInfo *Cluster return &poolStats, nil } -func checkForImagesInRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, poolName, namespaceName string) error { +// checkForImagesInRadosNamespace checks if there are any images or snapshots in the specified rados namespace. +// If there are images or snapshots, it returns true and an error with details. +func checkForImagesInRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, poolName, namespaceName string) (bool, error) { logger.Debugf("checking any images/snapshots present in pool %s/%s in k8s namespace %q", poolName, namespaceName, clusterInfo.Namespace) stats, err := getRadosNamespaceStatistics(context, clusterInfo, poolName, namespaceName) if err != nil { - return errors.Wrapf(err, "failed to list images/snapshots in pool %s/%s", poolName, namespaceName) + return false, errors.Wrapf(err, "failed to list images/snapshots in pool %s/%s", poolName, namespaceName) } if stats.Images.Count == 0 && stats.Images.SnapCount == 0 { logger.Infof("no images/snapshots present in pool %s/%s in k8s namespace %q", poolName, namespaceName, clusterInfo.Namespace) - return nil + return false, nil } - return errors.Errorf("pool %s/%s contains %d images and %d snapshots", poolName, namespaceName, stats.Images.Count, stats.Images.SnapCount) + return true, errors.Errorf("pool %s/%s contains %d images and %d snapshots", poolName, namespaceName, stats.Images.Count, stats.Images.SnapCount) } // DeleteRadosNamespace delete a rados namespace. -func DeleteRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, poolName, namespaceName string) error { - err := checkForImagesInRadosNamespace(context, clusterInfo, poolName, namespaceName) +func DeleteRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, poolName, namespaceName string) (bool, error) { + containsImages, err := checkForImagesInRadosNamespace(context, clusterInfo, poolName, namespaceName) if err != nil { - return errors.Wrapf(err, "failed to check if pool %s/%s has rbd images", poolName, namespaceName) + return containsImages, errors.Wrapf(err, "failed to check if pool %s/%s has rbd images", poolName, namespaceName) } logger.Infof("deleting rados namespace %s/%s in k8s namespace %q", poolName, namespaceName, clusterInfo.Namespace) args := []string{"namespace", "remove", "--pool", poolName, "--namespace", namespaceName} @@ -97,12 +99,12 @@ func DeleteRadosNamespace(context *clusterd.Context, clusterInfo *ClusterInfo, p if err != nil { code, ok := exec.ExitStatus(err) if !ok || code != int(syscall.ENOENT) { - return errors.Wrapf(err, "failed to delete rados namespace %s/%s. %s", poolName, namespaceName, output) + return false, errors.Wrapf(err, "failed to delete rados namespace %s/%s. %s", poolName, namespaceName, output) } } logger.Infof("successfully deleted rados namespace %s/%s in k8s namespace %q", poolName, namespaceName, clusterInfo.Namespace) - return nil + return false, nil } // ListRadosNamespacesInPool lists the rados namespaces in a pool diff --git a/pkg/operator/ceph/pool/controller.go b/pkg/operator/ceph/pool/controller.go index be3eaba23..efeaf6580 100644 --- a/pkg/operator/ceph/pool/controller.go +++ b/pkg/operator/ceph/pool/controller.go @@ -26,6 +26,7 @@ import ( "github.com/coreos/pkg/capnslog" cephclient "github.com/rook/rook/pkg/daemon/ceph/client" + "github.com/rook/rook/pkg/util/dependents" "github.com/rook/rook/pkg/util/exec" "github.com/pkg/errors" @@ -254,15 +255,10 @@ func (r *ReconcileCephBlockPool) reconcile(request reconcile.Request) (reconcile poolSpec := cephBlockPool.ToNamedPoolSpec() // DELETE: the CR was deleted if !cephBlockPool.GetDeletionTimestamp().IsZero() { - deps, err := cephBlockPoolDependents(r.context, r.clusterInfo, cephBlockPool) - if err != nil { - return reconcile.Result{}, *cephBlockPool, err - } - if !deps.Empty() { - err := reporting.ReportDeletionBlockedDueToDependents(r.opManagerContext, logger, r.client, cephBlockPool, deps) + if err := r.handleDeletionBlocked(cephBlockPool, &cephCluster); err != nil { return opcontroller.WaitForRequeueIfFinalizerBlocked, *cephBlockPool, err } - reporting.ReportDeletionNotBlockedDueToDependents(r.opManagerContext, logger, r.client, r.recorder, cephBlockPool) + // If the ceph block pool is still in the map, we must remove it during CR deletion // We must remove it first otherwise the checker will panic since the status/info will be nil r.cancelMirrorMonitoring(cephBlockPool) @@ -272,12 +268,6 @@ func (r *ReconcileCephBlockPool) reconcile(request reconcile.Request) (reconcile logger.Infof("deleting pool %q", poolSpec.Name) err = deletePool(r.context, clusterInfo, &poolSpec) if err != nil { - if opcontroller.ForceDeleteRequested(cephBlockPool.GetAnnotations()) { - cleanupErr := r.cleanup(cephBlockPool, &cephCluster) - if cleanupErr != nil { - return reconcile.Result{}, *cephBlockPool, errors.Wrapf(cleanupErr, "failed to create clean up job for ceph blockpool %q", cephBlockPool.Name) - } - } return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrapf(err, "failed to delete pool %q. ", cephBlockPool.Name) } @@ -417,6 +407,64 @@ func (r *ReconcileCephBlockPool) reconcile(request reconcile.Request) (reconcile return reconcile.Result{}, *cephBlockPool, nil } +// handlePoolDeletionBlocked updates the blockpool CR status with conditions about +// whether the pool is empty or has dependents that block deletion. +// If the pool is not empty and force deletion is specified, create a cleanup job +// to delete the images and snapshots forcefully. +func (r *ReconcileCephBlockPool) handleDeletionBlocked(cephBlockPool *cephv1.CephBlockPool, cephCluster *cephv1.CephCluster) error { + poolSpec := cephBlockPool.ToNamedPoolSpec() + deletionBlocked := false + + deps, err := cephBlockPoolDependents(r.context, r.clusterInfo, cephBlockPool) + if err != nil { + return err + } + var depCondition cephv1.Condition + if deps.Empty() { + _, _, depCondition = reporting.GenerateConditionUnblockedDueToDependents(cephBlockPool) + } else { + deletionBlocked = true + _, _, depCondition = reporting.GenerateConditionBlockedDueToDependents(cephBlockPool, deps) + } + logger.Info(depCondition.Message) + + radosNamespaces := deps.OfKind(radosNamespacesKeyName) + isEmpty, emptyMessage, err := cephclient.IsPoolEmpty(r.context, r.clusterInfo, poolSpec.Name, radosNamespaces) + if err != nil { + return err + } + var emptyCondition cephv1.Condition + if isEmpty { + emptyCondition = dependents.DeletionBlockedDueToNonEmptyPoolCondition(false, emptyMessage) + } else { + deletionBlocked = true + emptyCondition = dependents.DeletionBlockedDueToNonEmptyPoolCondition(true, emptyMessage) + } + logger.Info(emptyCondition.Message) + + nsName := types.NamespacedName{Namespace: cephBlockPool.Namespace, Name: cephBlockPool.Name} + err = reporting.UpdateStatusConditionsWithRetry( + r.opManagerContext, r.client, cephBlockPool, nsName, cephBlockPool.Kind, emptyCondition, depCondition) + if err != nil { + logger.Warningf("failed to update %q status with deletion blocked conditions: %v", nsName.String(), err) + } + + if !isEmpty { + // Force deletion if desired + if opcontroller.ForceDeleteRequested(cephBlockPool.GetAnnotations()) { + cleanupErr := r.cleanup(cephBlockPool, cephCluster) + if cleanupErr != nil { + return errors.Wrapf(cleanupErr, "failed to create clean up job for ceph blockpool %q", cephBlockPool.Name) + } + } + } + + if deletionBlocked { + return errors.Errorf("pool %q cannot be deleted because it is not empty or has dependents", cephBlockPool.Name) + } + return nil +} + func (r *ReconcileCephBlockPool) reconcileCreatePool(clusterInfo *cephclient.ClusterInfo, cephCluster *cephv1.ClusterSpec, cephBlockPool *cephv1.CephBlockPool) (reconcile.Result, error) { poolSpec := cephBlockPool.ToNamedPoolSpec() err := createPool(r.context, clusterInfo, cephCluster, &poolSpec) diff --git a/pkg/operator/ceph/pool/controller_test.go b/pkg/operator/ceph/pool/controller_test.go index 7891337c0..861f80afb 100644 --- a/pkg/operator/ceph/pool/controller_test.go +++ b/pkg/operator/ceph/pool/controller_test.go @@ -188,7 +188,7 @@ func TestDeletePool(t *testing.T) { // delete a pool that exists p := &cephv1.NamedPoolSpec{Name: "mypool"} err := deletePool(context, clusterInfo, p) - assert.Nil(t, err) + assert.NoError(t, err) // succeed even if the pool doesn't exist p = &cephv1.NamedPoolSpec{Name: "otherpool"} @@ -199,7 +199,7 @@ func TestDeletePool(t *testing.T) { failOnDelete = true p = &cephv1.NamedPoolSpec{Name: "mypool"} err = deletePool(context, clusterInfo, p) - assert.NotNil(t, err) + assert.Error(t, err) } // TestCephBlockPoolController runs ReconcileCephBlockPool.Reconcile() against a @@ -565,6 +565,131 @@ func TestCephBlockPoolController(t *testing.T) { }) } +func TestDeletionBlocked(t *testing.T) { + // A Pool resource with metadata and spec. + pool := &cephv1.CephBlockPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pool", + Namespace: "ns", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "CephBlockPool", + }, + Status: &cephv1.CephBlockPoolStatus{}, + } + object := []runtime.Object{pool} + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(cephv1.SchemeGroupVersion, + &cephv1.CephBlockPoolList{}, + &cephv1.CephBlockPoolRadosNamespaceList{}, + &cephv1.CephBlockPool{}, + &cephv1.CephBlockPoolRadosNamespace{}, + ) + blockImageCount := 0 + rnsImageCount := 0 + cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(object...).Build() + executor := &exectest.MockExecutor{ + MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) { + logger.Infof("Command: %s %v", command, args) + if args[0] == "pool" { + if args[1] == "stats" { + response := "{\"images\":{\"count\":%d,\"provisioned_bytes\":0,\"snap_count\":0},\"trash\":{\"count\":1,\"provisioned_bytes\":2048,\"snap_count\":0}}" + if args[4] == "--namespace" { + return fmt.Sprintf(response, rnsImageCount), nil + } else { + return fmt.Sprintf(response, blockImageCount), nil + } + } + } + return "not implemented", nil + }, + } + c := &clusterd.Context{ + Executor: executor, + Clientset: testop.New(t, 1), + RookClientset: rookclient.NewSimpleClientset(), + } + // Create a ReconcileCephBlockPool object with the scheme and fake client. + r := &ReconcileCephBlockPool{ + client: cl, + scheme: s, + context: c, + blockPoolContexts: make(map[string]*blockPoolHealth), + opManagerContext: context.TODO(), + recorder: record.NewFakeRecorder(5), + clusterInfo: cephclient.AdminTestClusterInfo("mycluster"), + } + cephCluster := &cephv1.CephCluster{} + t.Run("deletion is allowed with no images or rns", func(t *testing.T) { + err := r.handleDeletionBlocked(pool, cephCluster) + assert.NoError(t, err) + + result := &cephv1.CephBlockPool{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: pool.Name, Namespace: pool.Namespace}, result) + assert.NoError(t, err) + assert.Equal(t, pool.Name, result.Name) + assert.Equal(t, pool.Namespace, result.Namespace) + assert.Equal(t, 2, len(pool.Status.Conditions)) + assert.Equal(t, "PoolDeletionIsBlocked", string(pool.Status.Conditions[0].Type)) + assert.Equal(t, v1.ConditionFalse, pool.Status.Conditions[0].Status) + assert.Equal(t, "PoolEmpty", string(pool.Status.Conditions[0].Reason)) + assert.Equal(t, "DeletionIsBlocked", string(pool.Status.Conditions[1].Type)) + assert.Equal(t, v1.ConditionFalse, pool.Status.Conditions[1].Status) + assert.Equal(t, "ObjectHasNoDependents", string(pool.Status.Conditions[1].Reason)) + }) + t.Run("image prevents deletion", func(t *testing.T) { + blockImageCount = 1 + err := r.handleDeletionBlocked(pool, cephCluster) + assert.Error(t, err) + + result := &cephv1.CephBlockPool{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: pool.Name, Namespace: pool.Namespace}, result) + assert.NoError(t, err) + assert.Equal(t, pool.Name, result.Name) + assert.Equal(t, pool.Namespace, result.Namespace) + assert.Equal(t, 2, len(pool.Status.Conditions)) + assert.Equal(t, "PoolDeletionIsBlocked", string(pool.Status.Conditions[0].Type)) + assert.Equal(t, v1.ConditionTrue, pool.Status.Conditions[0].Status) + assert.Equal(t, "PoolNotEmpty", string(pool.Status.Conditions[0].Reason)) + assert.Equal(t, "DeletionIsBlocked", string(pool.Status.Conditions[1].Type)) + assert.Equal(t, v1.ConditionFalse, pool.Status.Conditions[1].Status) + assert.Equal(t, "ObjectHasNoDependents", string(pool.Status.Conditions[1].Reason)) + }) + t.Run("rados namespace prevents deletion", func(t *testing.T) { + radosNamespace := &cephv1.CephBlockPoolRadosNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rns", + Namespace: pool.Namespace, + }, + Spec: cephv1.CephBlockPoolRadosNamespaceSpec{ + BlockPoolName: pool.Name, + }, + } + _, err := c.RookClientset.CephV1().CephBlockPoolRadosNamespaces(pool.Namespace).Create(context.TODO(), radosNamespace, metav1.CreateOptions{}) + assert.NoError(t, err) + + // A radosnamespaces prevents deletion of the pool + blockImageCount = 0 + rnsImageCount = 1 + err = r.handleDeletionBlocked(pool, cephCluster) + assert.Error(t, err) + + result := &cephv1.CephBlockPool{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: pool.Name, Namespace: pool.Namespace}, result) + assert.NoError(t, err) + assert.Equal(t, pool.Name, result.Name) + assert.Equal(t, pool.Namespace, result.Namespace) + assert.Equal(t, 2, len(pool.Status.Conditions)) + assert.Equal(t, "PoolDeletionIsBlocked", string(pool.Status.Conditions[0].Type)) + assert.Equal(t, v1.ConditionTrue, pool.Status.Conditions[0].Status) + assert.Equal(t, "PoolNotEmpty", string(pool.Status.Conditions[0].Reason)) + assert.Equal(t, "DeletionIsBlocked", string(pool.Status.Conditions[1].Type)) + assert.Equal(t, v1.ConditionTrue, pool.Status.Conditions[1].Status) + assert.Equal(t, "ObjectHasDependents", string(pool.Status.Conditions[1].Reason)) + }) +} + func TestIsAnyRadosNamespaceMirrored(t *testing.T) { pool := "test" object := []runtime.Object{} diff --git a/pkg/operator/ceph/pool/dependents.go b/pkg/operator/ceph/pool/dependents.go index a0b8fb7ae..0d20f4e6a 100644 --- a/pkg/operator/ceph/pool/dependents.go +++ b/pkg/operator/ceph/pool/dependents.go @@ -20,16 +20,18 @@ import ( "fmt" "github.com/pkg/errors" - v1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1" + cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1" "github.com/rook/rook/pkg/clusterd" "github.com/rook/rook/pkg/daemon/ceph/client" "github.com/rook/rook/pkg/util/dependents" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const radosNamespacesKeyName = "CephBlockPoolRadosNamespaces" + // cephBlockPoolDependents returns the rbd namespaces (s) which exist in the rbd pool that should block // deletion. -func cephBlockPoolDependents(clusterdCtx *clusterd.Context, clusterInfo *client.ClusterInfo, blockpool *v1.CephBlockPool) (*dependents.DependentList, error) { +func cephBlockPoolDependents(clusterdCtx *clusterd.Context, clusterInfo *client.ClusterInfo, blockpool *cephv1.CephBlockPool) (*dependents.DependentList, error) { nsName := fmt.Sprintf("%s/%s", blockpool.Namespace, blockpool.Name) baseErrMsg := fmt.Sprintf("failed to get dependents of CephBlockPool %q", nsName) @@ -40,9 +42,9 @@ func cephBlockPoolDependents(clusterdCtx *clusterd.Context, clusterInfo *client. if err != nil { return deps, errors.Wrapf(err, "%s. failed to list CephBlockPoolRadosNamespace for CephBlockPool %q", baseErrMsg, nsName) } - for _, namespace := range namespaces.Items { + for i, namespace := range namespaces.Items { if namespace.Spec.BlockPoolName == blockpool.Name { - deps.Add("CephBlockPoolRadosNamespaces", namespace.Name) + deps.Add(radosNamespacesKeyName, cephv1.GetRadosNamespaceName(&namespaces.Items[i])) } logger.Debugf("found CephBlockPoolRadosNamespace %q that does not depend on CephBlockPool %q", namespace.Name, nsName) } diff --git a/pkg/operator/ceph/pool/radosnamespace/controller.go b/pkg/operator/ceph/pool/radosnamespace/controller.go index 49f1be3d6..11d5dc794 100644 --- a/pkg/operator/ceph/pool/radosnamespace/controller.go +++ b/pkg/operator/ceph/pool/radosnamespace/controller.go @@ -32,6 +32,7 @@ import ( "github.com/rook/rook/pkg/operator/ceph/csi" "github.com/rook/rook/pkg/operator/ceph/reporting" "github.com/rook/rook/pkg/operator/k8sutil" + "github.com/rook/rook/pkg/util/dependents" "github.com/coreos/pkg/capnslog" "github.com/pkg/errors" @@ -39,6 +40,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -71,6 +73,7 @@ type ReconcileCephBlockPoolRadosNamespace struct { clusterInfo *cephclient.ClusterInfo radosNamespaceContexts map[string]*mirrorHealth opManagerContext context.Context + recorder record.EventRecorder opConfig opcontroller.OperatorConfig } @@ -95,6 +98,7 @@ func newReconciler(mgr manager.Manager, context *clusterd.Context, opManagerCont context: context, radosNamespaceContexts: make(map[string]*mirrorHealth), opManagerContext: opManagerContext, + recorder: mgr.GetEventRecorderFor("rook-" + controllerName), opConfig: opConfig, } } @@ -135,46 +139,46 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { // otherwise upon completion it will remove the work from the queue. func (r *ReconcileCephBlockPoolRadosNamespace) Reconcile(context context.Context, request reconcile.Request) (reconcile.Result, error) { // workaround because the rook logging mechanism is not compatible with the controller-runtime logging interface - reconcileResponse, err := r.reconcile(request) + reconcileResponse, radosNamespace, err := r.reconcile(request) if err != nil { - logger.Errorf("failed to reconcile %q %v", request.NamespacedName, err) + logger.Errorf("failed to reconcile %q. %v", request.NamespacedName, err) } - return reconcileResponse, err + return reporting.ReportReconcileResult(logger, r.recorder, request, radosNamespace, reconcileResponse, err) } -func (r *ReconcileCephBlockPoolRadosNamespace) reconcile(request reconcile.Request) (reconcile.Result, error) { +func (r *ReconcileCephBlockPoolRadosNamespace) reconcile(request reconcile.Request) (reconcile.Result, *cephv1.CephBlockPoolRadosNamespace, error) { namespacedName := request.NamespacedName // Fetch the CephBlockPoolRadosNamespace instance - cephBlockPoolRadosNamespace := &cephv1.CephBlockPoolRadosNamespace{} - err := r.client.Get(r.opManagerContext, request.NamespacedName, cephBlockPoolRadosNamespace) + radosNamespace := &cephv1.CephBlockPoolRadosNamespace{} + err := r.client.Get(r.opManagerContext, request.NamespacedName, radosNamespace) if err != nil { if kerrors.IsNotFound(err) { logger.Debugf("cephBlockPoolRadosNamespace resource %q not found. Ignoring since object must be deleted.", namespacedName) - return reconcile.Result{}, nil + return reconcile.Result{}, radosNamespace, nil } // Error reading the object - requeue the request. - return reconcile.Result{}, errors.Wrap(err, "failed to get cephBlockPoolRadosNamespace") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to get cephBlockPoolRadosNamespace") } // Set a finalizer so we can do cleanup before the object goes away - generationUpdated, err := opcontroller.AddFinalizerIfNotPresent(r.opManagerContext, r.client, cephBlockPoolRadosNamespace) + generationUpdated, err := opcontroller.AddFinalizerIfNotPresent(r.opManagerContext, r.client, radosNamespace) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to add finalizer") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to add finalizer") } if generationUpdated { - logger.Infof("reconciling the rados namespace %q after adding finalizer", cephBlockPoolRadosNamespace.Name) - return reconcile.Result{}, nil + logger.Infof("reconciling the rados namespace %q after adding finalizer", radosNamespace.Name) + return reconcile.Result{}, radosNamespace, nil } // The CR was just created, initializing status fields - if cephBlockPoolRadosNamespace.Status == nil { + if radosNamespace.Status == nil { r.updateStatus(r.client, request.NamespacedName, cephv1.ConditionProgressing) } - poolAndRadosNamespaceName := cephBlockPoolRadosNamespace.Spec.BlockPoolName - if rns := getRadosNamespaceName(cephBlockPoolRadosNamespace); rns != "" { - poolAndRadosNamespaceName = fmt.Sprintf("%s/%s", cephBlockPoolRadosNamespace.Spec.BlockPoolName, rns) + poolAndRadosNamespaceName := radosNamespace.Spec.BlockPoolName + if rns := cephv1.GetRadosNamespaceName(radosNamespace); rns != "" { + poolAndRadosNamespaceName = fmt.Sprintf("%s/%s", radosNamespace.Spec.BlockPoolName, rns) } // Make sure a CephCluster is present otherwise do nothing @@ -186,94 +190,87 @@ func (r *ReconcileCephBlockPoolRadosNamespace) reconcile(request reconcile.Reque // 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 !cephBlockPoolRadosNamespace.GetDeletionTimestamp().IsZero() && !cephClusterExists { + if !radosNamespace.GetDeletionTimestamp().IsZero() && !cephClusterExists { // don't leak the health checker routine if we are force-deleting - r.cancelMirrorMonitoring(radosNamespaceChannelKeyName(cephBlockPoolRadosNamespace.Namespace, poolAndRadosNamespaceName)) + r.cancelMirrorMonitoring(radosNamespaceChannelKeyName(radosNamespace.Namespace, poolAndRadosNamespaceName)) // Remove finalizer - err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, cephBlockPoolRadosNamespace) + err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, radosNamespace) if err != nil { - return opcontroller.ImmediateRetryResult, errors.Wrap(err, "failed to remove finalizer") + return opcontroller.ImmediateRetryResult, radosNamespace, errors.Wrap(err, "failed to remove finalizer") } // Return and do not requeue. Successful deletion. - return reconcile.Result{}, nil + return reconcile.Result{}, radosNamespace, nil } - return reconcileResponse, nil + return reconcileResponse, radosNamespace, nil } // Populate clusterInfo during each reconcile r.clusterInfo, _, _, err = opcontroller.LoadClusterInfo(r.context, r.opManagerContext, request.NamespacedName.Namespace, &cephCluster.Spec) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to populate cluster info") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to populate cluster info") } r.clusterInfo.Context = r.opManagerContext // DELETE: the CR was deleted - if !cephBlockPoolRadosNamespace.GetDeletionTimestamp().IsZero() { + if !radosNamespace.GetDeletionTimestamp().IsZero() { logger.Debugf("delete cephBlockPoolRadosNamespace %q", namespacedName) // On external cluster, we don't delete the rados namespace, it has to be deleted manually if cephCluster.Spec.External.Enable { logger.Warning("external rados namespace %q deletion is not supported, delete it manually", namespacedName) } else { - err := r.deleteRadosNamespace(cephBlockPoolRadosNamespace) - if err != nil { + if containsImages, err := r.deleteRadosNamespace(radosNamespace, &cephCluster); err != nil { + if containsImages { + return opcontroller.WaitForRequeueIfFinalizerBlocked, radosNamespace, err + } if strings.Contains(err.Error(), opcontroller.UninitializedCephConfigError) { logger.Info(opcontroller.OperatorNotInitializedMessage) - return opcontroller.WaitForRequeueIfOperatorNotInitialized, nil + return opcontroller.WaitForRequeueIfOperatorNotInitialized, radosNamespace, nil } - if opcontroller.ForceDeleteRequested(cephBlockPoolRadosNamespace.GetAnnotations()) { - cleanupErr := r.cleanup(cephBlockPoolRadosNamespace, &cephCluster) - if cleanupErr != nil { - return reconcile.Result{}, errors.Wrapf(cleanupErr, "failed to create clean up job for ceph blockpool rados namespace %q", namespacedName) - } - } - return reconcile.Result{}, errors.Wrapf(err, "failed to delete ceph blockpool rados namespace %q", cephBlockPoolRadosNamespace.Name) + return reconcile.Result{}, radosNamespace, errors.Wrapf(err, "failed to delete ceph blockpool rados namespace %q", radosNamespace.Name) } // If the ceph block pool is still in the map, we must remove it during CR deletion // We must remove it first otherwise the checker will panic since the status/info will be nil - r.cancelMirrorMonitoring(radosNamespaceChannelKeyName(cephBlockPoolRadosNamespace.Namespace, poolAndRadosNamespaceName)) + r.cancelMirrorMonitoring(radosNamespaceChannelKeyName(radosNamespace.Namespace, poolAndRadosNamespaceName)) } - err = csi.SaveClusterConfig(r.context.Clientset, buildClusterID(cephBlockPoolRadosNamespace), cephCluster.Namespace, r.clusterInfo, nil) + err = csi.SaveClusterConfig(r.context.Clientset, buildClusterID(radosNamespace), cephCluster.Namespace, r.clusterInfo, nil) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to save cluster config") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to save cluster config") } // Remove finalizer - err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, cephBlockPoolRadosNamespace) + err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, radosNamespace) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to remove finalizer") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to remove finalizer") } // Return and do not requeue. Successful deletion. - return reconcile.Result{}, nil + return reconcile.Result{}, radosNamespace, nil } - cephBlockPoolRadosNamespaceName := cephBlockPoolRadosNamespace.Name - if cephBlockPoolRadosNamespace.Spec.Name != "" { - cephBlockPoolRadosNamespaceName = cephBlockPoolRadosNamespace.Spec.Name - } + radosNamespaceName := cephv1.GetRadosNamespaceName(radosNamespace) if cephCluster.Spec.External.Enable { logger.Debug("skip creating external radosnamespace in external mode, create it manually, the controller will assume it's there") - err = r.updateClusterConfig(cephBlockPoolRadosNamespace, cephCluster) + err = r.updateClusterConfig(radosNamespace, cephCluster) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to save cluster config") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to save cluster config") } r.updateStatus(r.client, namespacedName, cephv1.ConditionReady) if csi.EnableCSIOperator() { - err = csi.CreateUpdateClientProfileRadosNamespace(r.clusterInfo.Context, r.client, r.clusterInfo, cephBlockPoolRadosNamespaceName, buildClusterID(cephBlockPoolRadosNamespace), cephCluster.Name) + err = csi.CreateUpdateClientProfileRadosNamespace(r.clusterInfo.Context, r.client, r.clusterInfo, radosNamespaceName, buildClusterID(radosNamespace), cephCluster.Name) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to create ceph csi-op config CR for RadosNamespace") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to create ceph csi-op config CR for RadosNamespace") } } - return reconcile.Result{}, nil + return reconcile.Result{}, radosNamespace, nil } // cephversion check is only required for enabling mirroring - if cephBlockPoolRadosNamespace.Spec.Mirroring != nil { + if radosNamespace.Spec.Mirroring != nil { // Get CephCluster version cephVersion, err := opcontroller.GetImageVersion(cephCluster) if err != nil { - return opcontroller.ImmediateRetryResult, errors.Wrapf(err, "failed to fetch ceph version from cephcluster %q running in namespace %q", cephCluster.Name, cephCluster.Namespace) + return opcontroller.ImmediateRetryResult, radosNamespace, errors.Wrapf(err, "failed to fetch ceph version from cephcluster %q running in namespace %q", cephCluster.Name, cephCluster.Namespace) } if cephVersion != nil { r.clusterInfo.CephVersion = *cephVersion @@ -283,65 +280,56 @@ func (r *ReconcileCephBlockPoolRadosNamespace) reconcile(request reconcile.Reque // Build the NamespacedName to fetch the CephBlockPool and make sure it exists, if not we cannot // create the rados namespace cephBlockPool := &cephv1.CephBlockPool{} - pool := cephBlockPoolRadosNamespace.Spec.BlockPoolName + pool := radosNamespace.Spec.BlockPoolName cephBlockPoolNamespacedName := types.NamespacedName{Name: pool, Namespace: request.Namespace} err = r.client.Get(r.opManagerContext, cephBlockPoolNamespacedName, cephBlockPool) if err != nil { if kerrors.IsNotFound(err) { - return reconcile.Result{}, errors.Wrapf(err, "failed to fetch ceph blockpool %q, cannot create rados namespace %q", pool, cephBlockPoolRadosNamespace.Name) + return reconcile.Result{}, radosNamespace, errors.Wrapf(err, "failed to fetch ceph blockpool %q, cannot create rados namespace %q", pool, radosNamespace.Name) } // Error reading the object - requeue the request. - return reconcile.Result{}, errors.Wrap(err, "failed to get cephBlockPoolRadosNamespace") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to get cephBlockPoolRadosNamespace") } // If the cephBlockPool is not ready to accept commands, we should wait for it to be ready if cephBlockPool.Status.Phase != cephv1.ConditionReady { // We know the CR is present so it should a matter of second for it to become ready - return reconcile.Result{Requeue: true, RequeueAfter: 10 * time.Second}, errors.Wrapf(err, "failed to fetch ceph blockpool %q, cannot create rados namespace %q", pool, cephBlockPoolRadosNamespace.Name) + return reconcile.Result{Requeue: true, RequeueAfter: 10 * time.Second}, radosNamespace, errors.Wrapf(err, "failed to fetch ceph blockpool %q, cannot create rados namespace %q", pool, radosNamespace.Name) } // Create or Update rados namespace - err = r.createOrUpdateRadosNamespace(cephBlockPoolRadosNamespace) + err = r.createOrUpdateRadosNamespace(radosNamespace) if err != nil { if strings.Contains(err.Error(), opcontroller.UninitializedCephConfigError) { logger.Info(opcontroller.OperatorNotInitializedMessage) - return opcontroller.WaitForRequeueIfOperatorNotInitialized, nil + return opcontroller.WaitForRequeueIfOperatorNotInitialized, radosNamespace, nil } r.updateStatus(r.client, request.NamespacedName, cephv1.ConditionFailure) - return reconcile.Result{}, errors.Wrapf(err, "failed to create or update ceph pool rados namespace %q", cephBlockPoolRadosNamespace.Name) + return reconcile.Result{}, radosNamespace, errors.Wrapf(err, "failed to create or update ceph pool rados namespace %q", radosNamespace.Name) } - err = r.updateClusterConfig(cephBlockPoolRadosNamespace, cephCluster) + err = r.updateClusterConfig(radosNamespace, cephCluster) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to save cluster config") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to save cluster config") } - err = r.reconcileMirroring(cephBlockPoolRadosNamespace, cephBlockPool) + err = r.reconcileMirroring(radosNamespace, cephBlockPool) if err != nil { - return reconcile.Result{}, err + return reconcile.Result{}, radosNamespace, err } r.updateStatus(r.client, namespacedName, cephv1.ConditionReady) if csi.EnableCSIOperator() { - err = csi.CreateUpdateClientProfileRadosNamespace(r.clusterInfo.Context, r.client, r.clusterInfo, cephBlockPoolRadosNamespaceName, buildClusterID(cephBlockPoolRadosNamespace), cephCluster.Name) + err = csi.CreateUpdateClientProfileRadosNamespace(r.clusterInfo.Context, r.client, r.clusterInfo, radosNamespaceName, buildClusterID(radosNamespace), cephCluster.Name) if err != nil { - return reconcile.Result{}, errors.Wrap(err, "failed to create ceph csi-op config CR for RadosNamespace") + return reconcile.Result{}, radosNamespace, errors.Wrap(err, "failed to create ceph csi-op config CR for RadosNamespace") } } // Return and do not requeue logger.Debugf("done reconciling cephBlockPoolRadosNamespace %q", namespacedName) - return reconcile.Result{}, nil -} - -func getRadosNamespaceName(cephBlockPoolRadosNamespace *cephv1.CephBlockPoolRadosNamespace) string { - if cephBlockPoolRadosNamespace.Spec.Name == cephclient.ImplicitNamespaceKey { - return cephclient.ImplicitNamespaceVal - } else if cephBlockPoolRadosNamespace.Spec.Name != "" { - return cephBlockPoolRadosNamespace.Spec.Name - } - return cephBlockPoolRadosNamespace.Name + return reconcile.Result{}, radosNamespace, nil } func (r *ReconcileCephBlockPoolRadosNamespace) updateClusterConfig(cephBlockPoolRadosNamespace *cephv1.CephBlockPoolRadosNamespace, cephCluster cephv1.CephCluster) error { @@ -353,7 +341,7 @@ func (r *ReconcileCephBlockPoolRadosNamespace) updateClusterConfig(cephBlockPool ClusterInfo: cephcsi.ClusterInfo{ Monitors: csi.MonEndpoints(r.clusterInfo.AllMonitors(), cephCluster.Spec.RequireMsgr2()), RBD: cephcsi.RBD{ - RadosNamespace: getRadosNamespaceName(cephBlockPoolRadosNamespace), + RadosNamespace: cephv1.GetRadosNamespaceName(cephBlockPoolRadosNamespace), }, CephFS: cephcsi.CephFS{ KernelMountOptions: r.clusterInfo.CSIDriverSpec.CephFS.KernelMountOptions, @@ -382,11 +370,11 @@ func (r *ReconcileCephBlockPoolRadosNamespace) createOrUpdateRadosNamespace(ceph namespacedName := fmt.Sprintf("%s/%s", cephBlockPoolRadosNamespace.Namespace, cephBlockPoolRadosNamespace.Name) logger.Infof("creating ceph blockpool rados namespace %q", namespacedName) - if getRadosNamespaceName(cephBlockPoolRadosNamespace) == "" { + if cephv1.GetRadosNamespaceName(cephBlockPoolRadosNamespace) == "" { logger.Info("can't create empty radosnamespace %q in the namespace %q as it is already present", cephBlockPoolRadosNamespace.Name, cephBlockPoolRadosNamespace.Namespace) return nil } - err := cephclient.CreateRadosNamespace(r.context, r.clusterInfo, cephBlockPoolRadosNamespace.Spec.BlockPoolName, getRadosNamespaceName(cephBlockPoolRadosNamespace)) + err := cephclient.CreateRadosNamespace(r.context, r.clusterInfo, cephBlockPoolRadosNamespace.Spec.BlockPoolName, cephv1.GetRadosNamespaceName(cephBlockPoolRadosNamespace)) if err != nil { return errors.Wrapf(err, "failed to create ceph blockpool rados namespace %q", cephBlockPoolRadosNamespace.Name) } @@ -395,21 +383,53 @@ func (r *ReconcileCephBlockPoolRadosNamespace) createOrUpdateRadosNamespace(ceph } // Delete the ceph blockpool rados namespace -func (r *ReconcileCephBlockPoolRadosNamespace) deleteRadosNamespace(cephBlockPoolRadosNamespace *cephv1.CephBlockPoolRadosNamespace) error { - namespacedName := fmt.Sprintf("%s/%s", cephBlockPoolRadosNamespace.Namespace, cephBlockPoolRadosNamespace.Name) - logger.Infof("deleting ceph blockpool rados namespace object %q", namespacedName) +func (r *ReconcileCephBlockPoolRadosNamespace) deleteRadosNamespace(radosNamespace *cephv1.CephBlockPoolRadosNamespace, cephCluster *cephv1.CephCluster) (bool, error) { + nsName := types.NamespacedName{Namespace: radosNamespace.Namespace, Name: radosNamespace.Name} + logger.Infof("deleting rados namespace %q", nsName.String()) - if getRadosNamespaceName(cephBlockPoolRadosNamespace) == "" { - logger.Info("can't delete empty radosnamepace as it present by default.") - return nil + name := cephv1.GetRadosNamespaceName(radosNamespace) + if name == "" { + logger.Info("no need to delete implicit radosnamepace") + return false, nil } - if err := cephclient.DeleteRadosNamespace(r.context, r.clusterInfo, cephBlockPoolRadosNamespace.Spec.BlockPoolName, getRadosNamespaceName(cephBlockPoolRadosNamespace)); err != nil { - return errors.Wrapf(err, "failed to delete ceph blockpool rados namespace %q", cephBlockPoolRadosNamespace.Name) + containsImages, deleteErr := cephclient.DeleteRadosNamespace(r.context, r.clusterInfo, radosNamespace.Spec.BlockPoolName, name) + // If deleteErr is not nil, it means the deletion failed, but we still want to + // report a condition whether the rados namespace contains images + if containsImages { + var emptyCondition cephv1.Condition + if containsImages { + emptyCondition = dependents.DeletionBlockedDueToNonEmptyRadosNSCondition( + true, + fmt.Sprintf("rados namespace %q contains images or snapshots and cannot be deleted", radosNamespace.Name)) + } else { + emptyCondition = dependents.DeletionBlockedDueToNonEmptyRadosNSCondition( + false, + fmt.Sprintf("rados namespace %q is empty and can be deleted", radosNamespace.Name)) + } + logger.Info(emptyCondition.Message) + + err := reporting.UpdateStatusConditionsWithRetry( + r.opManagerContext, r.client, radosNamespace, nsName, radosNamespace.Kind, emptyCondition) + if err != nil { + logger.Warningf("failed to update %q status with deletion blocked conditions: %v", nsName.String(), err) + } + + // Force deletion if desired + if opcontroller.ForceDeleteRequested(radosNamespace.GetAnnotations()) { + cleanupErr := r.cleanup(radosNamespace, cephCluster) + if cleanupErr != nil { + return containsImages, errors.Wrapf(cleanupErr, "failed to create clean up job for rados namespace %q", radosNamespace.Name) + } + } } - logger.Infof("deleted ceph blockpool rados namespace %q", namespacedName) - return nil + if deleteErr != nil { + return containsImages, errors.Wrapf(deleteErr, "failed to delete rados namespace %q", radosNamespace.Name) + } + + logger.Infof("deleted rados namespace %q", nsName.String()) + return false, nil } // updateStatus updates an object with a given status @@ -437,7 +457,7 @@ func (r *ReconcileCephBlockPoolRadosNamespace) updateStatus(client client.Client } func buildClusterID(cephBlockPoolRadosNamespace *cephv1.CephBlockPoolRadosNamespace) string { - clusterID := fmt.Sprintf("%s-%s-block-%s", cephBlockPoolRadosNamespace.Namespace, cephBlockPoolRadosNamespace.Spec.BlockPoolName, getRadosNamespaceName(cephBlockPoolRadosNamespace)) + clusterID := fmt.Sprintf("%s-%s-block-%s", cephBlockPoolRadosNamespace.Namespace, cephBlockPoolRadosNamespace.Spec.BlockPoolName, cephv1.GetRadosNamespaceName(cephBlockPoolRadosNamespace)) return k8sutil.Hash(clusterID) } @@ -445,7 +465,7 @@ func (r *ReconcileCephBlockPoolRadosNamespace) cleanup(radosNamespace *cephv1.Ce logger.Infof("starting cleanup of the ceph resources for radosNamespace %q in namespace %q", radosNamespace.Name, radosNamespace.Namespace) cleanupConfig := map[string]string{ opcontroller.CephBlockPoolNameEnv: radosNamespace.Spec.BlockPoolName, - opcontroller.CephBlockPoolRadosNamespaceEnv: getRadosNamespaceName(radosNamespace), + opcontroller.CephBlockPoolRadosNamespaceEnv: cephv1.GetRadosNamespaceName(radosNamespace), } cleanup := opcontroller.NewResourceCleanup(radosNamespace, cephCluster, r.opConfig.Image, cleanupConfig) jobName := k8sutil.TruncateNodeNameForJob("cleanup-radosnamespace-%s", fmt.Sprintf("%s-%s", radosNamespace.Spec.BlockPoolName, radosNamespace.Name)) @@ -461,8 +481,8 @@ func checkBlockPoolMirroring(cephBlockPool *cephv1.CephBlockPool) bool { } func (r *ReconcileCephBlockPoolRadosNamespace) reconcileMirroring(cephBlockPoolRadosNamespace *cephv1.CephBlockPoolRadosNamespace, cephBlockPool *cephv1.CephBlockPool) error { - poolAndRadosNamespaceName := fmt.Sprintf("%s/%s", cephBlockPool.Name, getRadosNamespaceName(cephBlockPoolRadosNamespace)) - if getRadosNamespaceName(cephBlockPoolRadosNamespace) == "" { + poolAndRadosNamespaceName := fmt.Sprintf("%s/%s", cephBlockPool.Name, cephv1.GetRadosNamespaceName(cephBlockPoolRadosNamespace)) + if cephv1.GetRadosNamespaceName(cephBlockPoolRadosNamespace) == "" { poolAndRadosNamespaceName = cephBlockPool.Name } diff --git a/pkg/operator/ceph/pool/radosnamespace/controller_test.go b/pkg/operator/ceph/pool/radosnamespace/controller_test.go index dad2e6489..e0df465a4 100644 --- a/pkg/operator/ceph/pool/radosnamespace/controller_test.go +++ b/pkg/operator/ceph/pool/radosnamespace/controller_test.go @@ -27,7 +27,6 @@ import ( rookclient "github.com/rook/rook/pkg/client/clientset/versioned/fake" "github.com/rook/rook/pkg/client/clientset/versioned/scheme" "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/csi" "github.com/rook/rook/pkg/operator/k8sutil" @@ -38,6 +37,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -106,6 +106,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { context: c, opManagerContext: ctx, opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, + recorder: record.NewFakeRecorder(5), } // Mock request to simulate Reconcile() being called on an event for a @@ -142,6 +143,9 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { s.AddKnownTypes(cephv1.SchemeGroupVersion, &cephv1.CephCluster{}, &cephv1.CephClusterList{}) t.Run("error - no ceph cluster", func(t *testing.T) { + // Create pool for updating status + _, err := c.RookClientset.CephV1().CephBlockPoolRadosNamespaces(namespace).Create(ctx, cephBlockPoolRadosNamespace, metav1.CreateOptions{}) + assert.NoError(t, err) res, err := r.Reconcile(ctx, req) assert.NoError(t, err) assert.True(t, res.Requeue) @@ -152,7 +156,11 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { // Create a fake client to mock API calls. cl = fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(object...).Build() // Create a ReconcileCephBlockPoolRadosNamespace object with the scheme and fake client. - r = &ReconcileCephBlockPoolRadosNamespace{client: cl, scheme: s, context: c, opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}} + r = &ReconcileCephBlockPoolRadosNamespace{ + client: cl, scheme: s, context: c, opManagerContext: context.TODO(), + recorder: record.NewFakeRecorder(5), + opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, + } res, err := r.Reconcile(ctx, req) assert.NoError(t, err) assert.True(t, res.Requeue) @@ -227,6 +235,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, radosNamespaceContexts: make(map[string]*mirrorHealth), + recorder: record.NewFakeRecorder(5), } // Enable CSI @@ -279,6 +288,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { context: c, opManagerContext: ctx, opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, + recorder: record.NewFakeRecorder(5), } // Enable CSI @@ -346,6 +356,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, radosNamespaceContexts: make(map[string]*mirrorHealth), + recorder: record.NewFakeRecorder(5), } res, err := r.Reconcile(ctx, req) @@ -392,6 +403,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, radosNamespaceContexts: make(map[string]*mirrorHealth), + recorder: record.NewFakeRecorder(5), } res, err := r.Reconcile(ctx, req) @@ -442,6 +454,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, radosNamespaceContexts: make(map[string]*mirrorHealth), + recorder: record.NewFakeRecorder(5), } res, err := r.Reconcile(ctx, req) @@ -492,6 +505,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, radosNamespaceContexts: make(map[string]*mirrorHealth), + recorder: record.NewFakeRecorder(5), } res, err := r.Reconcile(ctx, req) @@ -544,6 +558,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, radosNamespaceContexts: make(map[string]*mirrorHealth), + recorder: record.NewFakeRecorder(5), } res, err := r.Reconcile(ctx, req) @@ -600,6 +615,7 @@ func TestCephBlockPoolRadosNamespaceController(t *testing.T) { opManagerContext: context.TODO(), opConfig: opcontroller.OperatorConfig{Image: "ceph/ceph:v14.2.9"}, radosNamespaceContexts: make(map[string]*mirrorHealth), + recorder: record.NewFakeRecorder(5), } res, err := r.Reconcile(ctx, req) @@ -629,9 +645,9 @@ func TestGetRadosNamespaceName(t *testing.T) { "implicit-namespace", cephv1.CephBlockPoolRadosNamespace{ ObjectMeta: metav1.ObjectMeta{Name: "cr-name1"}, - Spec: cephv1.CephBlockPoolRadosNamespaceSpec{Name: cephclient.ImplicitNamespaceKey}, + Spec: cephv1.CephBlockPoolRadosNamespaceSpec{Name: cephv1.ImplicitNamespaceKey}, }, - cephclient.ImplicitNamespaceVal, + cephv1.ImplicitNamespaceVal, }, { "valid-namespace", @@ -653,8 +669,8 @@ func TestGetRadosNamespaceName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := getRadosNamespaceName(&tt.args); got != tt.want { - t.Errorf("getRadosNamespaceName() = %v, want %v", got, tt.want) + if got := cephv1.GetRadosNamespaceName(&tt.args); got != tt.want { + t.Errorf("GetRadosNamespaceName() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/operator/ceph/reporting/reporting.go b/pkg/operator/ceph/reporting/reporting.go index 8a3cf28a4..0c4c9a0e6 100644 --- a/pkg/operator/ceph/reporting/reporting.go +++ b/pkg/operator/ceph/reporting/reporting.go @@ -169,6 +169,28 @@ func ReportReconcileResult( return reconcileResponse, err } +func GenerateConditionBlockedDueToDependents(obj statusConditionGetter, deps *dependents.DependentList) (string, types.NamespacedName, cephv1.Condition) { + kind := obj.GetObjectKind().GroupVersionKind().Kind + nsName := types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } + blockedMsg := deps.StringWithHeader("%s %q will not be deleted until all dependents are removed", kind, nsName.String()) + + return kind, nsName, dependents.DeletionBlockedDueToDependentsCondition(true, blockedMsg) +} + +func GenerateConditionUnblockedDueToDependents(obj statusConditionGetter) (string, types.NamespacedName, cephv1.Condition) { + kind := obj.GetObjectKind().GroupVersionKind().Kind + nsName := types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } + safeMsg := fmt.Sprintf("%s %q has no dependent custom resources blocking deletion", kind, nsName.String()) + + return kind, nsName, dependents.DeletionBlockedDueToDependentsCondition(false, safeMsg) +} + // ReportDeletionBlockedDueToDependents reports that deletion of a Rook-Ceph object is blocked due // to the given dependents in 3 ways: // 1. to the given logger @@ -177,33 +199,33 @@ func ReportReconcileResult( func ReportDeletionBlockedDueToDependents( ctx context.Context, logger *capnslog.PackageLogger, client client.Client, obj statusConditionGetter, deps *dependents.DependentList, ) error { - kind := obj.GetObjectKind().GroupVersionKind().Kind - nsName := types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - } - blockedMsg := deps.StringWithHeader("%s %q will not be deleted until all dependents are removed", kind, nsName.String()) + kind, nsName, blockedCond := GenerateConditionBlockedDueToDependents(obj, deps) // 1. log - logger.Info(blockedMsg) + logger.Info(blockedCond.Message) // 2. condition - blockedCond := dependents.DeletionBlockedDueToDependentsCondition(true, blockedMsg) - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := client.Get(ctx, nsName, obj); err != nil { - return errors.Wrapf(err, "failed to get latest %s %q", kind, nsName.String()) - } - if err := UpdateStatusCondition(client, obj, blockedCond); err != nil { - return err - } - return nil - }) - if err != nil { - return errors.Wrapf(err, "on condition %s", blockedMsg) + if err := UpdateStatusConditionsWithRetry(ctx, client, obj, nsName, kind, blockedCond); err != nil { + return err } // 3. error for later FailedReconcile message - return errors.New(blockedMsg) + return errors.New(blockedCond.Message) +} + +func UpdateStatusConditionsWithRetry( + ctx context.Context, client client.Client, obj statusConditionGetter, + nsName types.NamespacedName, kind string, conditions ...cephv1.Condition, +) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := client.Get(ctx, nsName, obj); err != nil { + return errors.Wrapf(err, "failed to get latest %s %q", kind, nsName.String()) + } + if err := UpdateStatusCondition(client, obj, conditions...); err != nil { + return errors.Wrapf(err, "failed to update %s %q status conditions", kind, nsName.String()) + } + return nil + }) } // ReportDeletionNotBlockedDueToDependents reports that deletion of a Rook-Ceph object is proceeding @@ -214,32 +236,17 @@ func ReportDeletionBlockedDueToDependents( func ReportDeletionNotBlockedDueToDependents( ctx context.Context, logger *capnslog.PackageLogger, client client.Client, recorder record.EventRecorder, obj statusConditionGetter, ) { - kind := obj.GetObjectKind().GroupVersionKind().Kind - nsName := types.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - } - safeMsg := fmt.Sprintf("%s %q can be deleted safely", kind, nsName.String()) + kind, nsName, unblockedCond := GenerateConditionUnblockedDueToDependents(obj) deletingMsg := fmt.Sprintf("deleting %s %q", kind, nsName.String()) // 1. log - logger.Infof("%s. %s", safeMsg, deletingMsg) + logger.Infof("%s. %s", unblockedCond.Message, deletingMsg) // 2. event recorder.Event(obj, corev1.EventTypeNormal, string(cephv1.DeletingReason), deletingMsg) // 3. condition - unblockedCond := dependents.DeletionBlockedDueToDependentsCondition(false, safeMsg) - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := client.Get(ctx, nsName, obj); err != nil { - return errors.Wrapf(err, "failed to get latest %s %q", kind, nsName.String()) - } - if err := UpdateStatusCondition(client, obj, unblockedCond); err != nil { - return err - } - return nil - }) - if err != nil { + if err := UpdateStatusConditionsWithRetry(ctx, client, obj, nsName, kind, unblockedCond); err != nil { logger.Warningf("continuing deletion of %s %q without setting the condition. %v", kind, nsName.String(), err) } } diff --git a/pkg/operator/ceph/reporting/status.go b/pkg/operator/ceph/reporting/status.go index 9ba0b0447..874e129fe 100644 --- a/pkg/operator/ceph/reporting/status.go +++ b/pkg/operator/ceph/reporting/status.go @@ -51,14 +51,17 @@ func UpdateStatus(client client.Client, obj client.Object) error { // UpdateStatusCondition updates (or adds to) the status condition to the given object. The object // is updated with the latest version from the server on a successful update. func UpdateStatusCondition( - client client.Client, obj statusConditionGetter, newCond cephv1.Condition, + client client.Client, obj statusConditionGetter, newConditions ...cephv1.Condition, ) error { kind := obj.GetObjectKind().GroupVersionKind().Kind nsName := fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()) - cephv1.SetStatusCondition(obj.GetStatusConditions(), newCond) + for _, newCondition := range newConditions { + cephv1.SetStatusCondition(obj.GetStatusConditions(), newCondition) + } + if err := UpdateStatus(client, obj); err != nil { - return errors.Wrapf(err, "failed to update %s %q status condition %s=%s", kind, nsName, newCond.Type, newCond.Status) + return errors.Wrapf(err, "failed to update %s %q status conditions", kind, nsName) } return nil diff --git a/pkg/operator/ceph/reporting/status_test.go b/pkg/operator/ceph/reporting/status_test.go index 522438c78..5aa8b4679 100644 --- a/pkg/operator/ceph/reporting/status_test.go +++ b/pkg/operator/ceph/reporting/status_test.go @@ -114,20 +114,38 @@ func TestUpdateStatusCondition(t *testing.T) { Phase: cephv1.ConditionDeleting, }, } + fakeBlock := &cephv1.CephBlockPool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypool", + Namespace: "rook-ceph", + Finalizers: []string{}, + }, + Status: &cephv1.CephBlockPoolStatus{ + Phase: cephv1.ConditionDeleting, + }, + } nsName := types.NamespacedName{ Namespace: fakeObject.Namespace, Name: fakeObject.Name, } + poolName := types.NamespacedName{ + Namespace: fakeBlock.Namespace, + Name: fakeBlock.Name, + } s := scheme.Scheme - s.AddKnownTypes(cephv1.SchemeGroupVersion, fakeObject) - cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(fakeObject.DeepCopy()).Build() + s.AddKnownTypes(cephv1.SchemeGroupVersion, fakeObject, fakeBlock) + cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(fakeObject.DeepCopy(), fakeBlock.DeepCopy()).Build() - // get version of the object in the fake object tracker + // get version of the objects in the fake object tracker getObj := &cephv1.CephObjectStore{} err := cl.Get(context.TODO(), nsName, getObj) assert.NoError(t, err) assert.Zero(t, len(getObj.Status.Conditions)) + getBlock := &cephv1.CephBlockPool{} + err = cl.Get(context.TODO(), poolName, getBlock) + assert.NoError(t, err) + assert.Zero(t, len(getBlock.Status.Conditions)) t.Run("add new status", func(t *testing.T) { getObj := &cephv1.CephObjectStore{} @@ -152,6 +170,40 @@ func TestUpdateStatusCondition(t *testing.T) { assert.Equal(t, "start", cond.Message) }) + t.Run("add two statuses", func(t *testing.T) { + getObj := &cephv1.CephBlockPool{} + err := cl.Get(context.TODO(), poolName, getObj) + assert.NoError(t, err) + + blockedCond := cephv1.Condition{ + Type: cephv1.ConditionDeletionIsBlocked, + Status: v1.ConditionTrue, + Reason: cephv1.ConditionReason("dep"), + Message: "there is a dependency", + } + emptyCond := cephv1.Condition{ + Type: cephv1.ConditionPoolDeletionIsBlocked, + Status: v1.ConditionTrue, + Reason: cephv1.ConditionReason("notempty"), + Message: "images are in the pool", + } + + err = UpdateStatusCondition(cl, getObj, blockedCond, emptyCond) + assert.NoError(t, err) + + err = cl.Get(context.TODO(), poolName, getObj) + assert.NoError(t, err) + cond := getObj.Status.Conditions[0] + assert.Equal(t, v1.ConditionTrue, cond.Status) + assert.Equal(t, blockedCond.Reason, cond.Reason) + assert.Equal(t, blockedCond.Message, cond.Message) + + cond = getObj.Status.Conditions[1] + assert.Equal(t, v1.ConditionTrue, cond.Status) + assert.Equal(t, emptyCond.Reason, cond.Reason) + assert.Equal(t, emptyCond.Message, cond.Message) + }) + t.Run("update status", func(t *testing.T) { getObj := &cephv1.CephObjectStore{} err := cl.Get(context.TODO(), nsName, getObj) diff --git a/pkg/util/dependents/dependents.go b/pkg/util/dependents/dependents.go index 262b364b5..860ee7987 100644 --- a/pkg/util/dependents/dependents.go +++ b/pkg/util/dependents/dependents.go @@ -40,6 +40,36 @@ func DeletionBlockedDueToDependentsCondition(blocked bool, message string) cephv } } +func DeletionBlockedDueToNonEmptyPoolCondition(blocked bool, message string) cephv1.Condition { + status := corev1.ConditionFalse + reason := cephv1.PoolEmptyReason + if blocked { + status = corev1.ConditionTrue + reason = cephv1.PoolNotEmptyReason + } + return cephv1.Condition{ + Type: cephv1.ConditionPoolDeletionIsBlocked, + Status: status, + Reason: reason, + Message: message, + } +} + +func DeletionBlockedDueToNonEmptyRadosNSCondition(blocked bool, message string) cephv1.Condition { + status := corev1.ConditionFalse + reason := cephv1.RadosNamespaceEmptyReason + if blocked { + status = corev1.ConditionTrue + reason = cephv1.RadosNamespaceNotEmptyReason + } + return cephv1.Condition{ + Type: cephv1.ConditionRadosNSDeletionIsBlocked, + Status: status, + Reason: reason, + Message: message, + } +} + // A DependentList represents a list of dependents of a resource. Each dependent has a plural Kind // and a list of names of dependent resources. type DependentList struct {