Repository: scylladb/scylla-operator
Pull request #1070 (2022-10-14 13:02:05)

Add Status Conditions
**Description of your changes:**
This PR adds 3 main conditions that help users determine the ScyllaCluster status.
 - `Available` (whether all replicas are up)
 - `Progressing` (whether the controller is making changes to reach the desired state)
 - `Degraded` (whether the controller hit any issues)

A rollout is done when `Available==True`, `Progressing==False`, `Degraded==False`. This gives user an easy way to interpret the status and possibly use upstream tooling, like `kubectl wait --for=condition=`.

Conditions also allow determining the processing state and the errors manifested into degraded state without consulting operator logs (which only admins can do).

This is also needed for #1021 where it allows interpreting state for multiple controller that consume it's outputs.



Patch:
diff --git a/examples/common/operator.yaml b/examples/common/operator.yaml
index 104639bb2..888cc04de 100644
--- a/examples/common/operator.yaml
+++ b/examples/common/operator.yaml
@@ -2138,6 +2138,50 @@ spec:
                         type: array
                     type: object
                   type: array
+                conditions:
+                  description: conditions hold conditions describing ScyllaCluster state. To determine whether a cluster rollout is finished, look for Available=True,Progressing=False,Degraded=False.
+                  items:
+                    description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions.  For example, \n \ttype FooStatus struct{ \t    // Represents the observations of a foo's current state. \t    // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" \t    // +patchMergeKey=type \t    // +patchStrategy=merge \t    // +listType=map \t    // +listMapKey=type \t    Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n \t    // other fields \t}"
+                    properties:
+                      lastTransitionTime:
+                        description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                        format: date-time
+                        type: string
+                      message:
+                        description: message is a human readable message indicating details about the transition. This may be an empty string.
+                        maxLength: 32768
+                        type: string
+                      observedGeneration:
+                        description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
+                        format: int64
+                        minimum: 0
+                        type: integer
+                      reason:
+                        description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
+                        maxLength: 1024
+                        minLength: 1
+                        pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                        type: string
+                      status:
+                        description: status of the condition, one of True, False, Unknown.
+                        enum:
+                          - "True"
+                          - "False"
+                          - Unknown
+                        type: string
+                      type:
+                        description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                        maxLength: 316
+                        pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                        type: string
+                    required:
+                      - lastTransitionTime
+                      - message
+                      - reason
+                      - status
+                      - type
+                    type: object
+                  type: array
                 managerId:
                   description: managerId contains ID under which cluster was registered in Scylla Manager.
                   type: string
diff --git a/pkg/api/scylla/v1/cluster_types.go b/pkg/api/scylla/v1/cluster_types.go
index a14eaa801..b370bd87d 100644
--- a/pkg/api/scylla/v1/cluster_types.go
+++ b/pkg/api/scylla/v1/cluster_types.go
@@ -430,8 +430,18 @@ type ScyllaClusterStatus struct {
 
 	// upgrade reflects state of ongoing upgrade procedure.
 	Upgrade *UpgradeStatus `json:"upgrade,omitempty"`
+
+	// conditions hold conditions describing ScyllaCluster state.
+	// To determine whether a cluster rollout is finished, look for Available=True,Progressing=False,Degraded=False.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
+const (
+	AvailableCondition   = "Available"
+	ProgressingCondition = "Progressing"
+	DegradedCondition    = "Degraded"
+)
+
 // UpgradeStatus contains the internal state of an ongoing upgrade procedure.
 // Do not rely on these internal values externally. They are meant for keeping an internal state
 // and their values are subject to change within the limits of API compatibility.
diff --git a/pkg/api/scylla/v1/scylla.scylladb.com_scyllaclusters.yaml b/pkg/api/scylla/v1/scylla.scylladb.com_scyllaclusters.yaml
index bb6fd5957..12dfa5b5e 100644
--- a/pkg/api/scylla/v1/scylla.scylladb.com_scyllaclusters.yaml
+++ b/pkg/api/scylla/v1/scylla.scylladb.com_scyllaclusters.yaml
@@ -1852,6 +1852,50 @@ spec:
                         type: array
                     type: object
                   type: array
+                conditions:
+                  description: conditions hold conditions describing ScyllaCluster state. To determine whether a cluster rollout is finished, look for Available=True,Progressing=False,Degraded=False.
+                  items:
+                    description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions.  For example, \n \ttype FooStatus struct{ \t    // Represents the observations of a foo's current state. \t    // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" \t    // +patchMergeKey=type \t    // +patchStrategy=merge \t    // +listType=map \t    // +listMapKey=type \t    Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n \t    // other fields \t}"
+                    properties:
+                      lastTransitionTime:
+                        description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                        format: date-time
+                        type: string
+                      message:
+                        description: message is a human readable message indicating details about the transition. This may be an empty string.
+                        maxLength: 32768
+                        type: string
+                      observedGeneration:
+                        description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
+                        format: int64
+                        minimum: 0
+                        type: integer
+                      reason:
+                        description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
+                        maxLength: 1024
+                        minLength: 1
+                        pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                        type: string
+                      status:
+                        description: status of the condition, one of True, False, Unknown.
+                        enum:
+                          - "True"
+                          - "False"
+                          - Unknown
+                        type: string
+                      type:
+                        description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                        maxLength: 316
+                        pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                        type: string
+                    required:
+                      - lastTransitionTime
+                      - message
+                      - reason
+                      - status
+                      - type
+                    type: object
+                  type: array
                 managerId:
                   description: managerId contains ID under which cluster was registered in Scylla Manager.
                   type: string
diff --git a/pkg/api/scylla/v1/zz_generated.deepcopy.go b/pkg/api/scylla/v1/zz_generated.deepcopy.go
index 3462e9a50..7cb5be56c 100644
--- a/pkg/api/scylla/v1/zz_generated.deepcopy.go
+++ b/pkg/api/scylla/v1/zz_generated.deepcopy.go
@@ -7,6 +7,7 @@ package v1
 
 import (
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -585,6 +586,13 @@ func (in *ScyllaClusterStatus) DeepCopyInto(out *ScyllaClusterStatus) {
 		*out = new(UpgradeStatus)
 		**out = **in
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]metav1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 	return
 }
 
diff --git a/pkg/controller/scyllacluster/conditions.go b/pkg/controller/scyllacluster/conditions.go
new file mode 100644
index 000000000..30db24e8f
--- /dev/null
+++ b/pkg/controller/scyllacluster/conditions.go
@@ -0,0 +1,19 @@
+package scyllacluster
+
+const (
+	serviceAccountControllerProgressingCondition = "ServiceAccountControllerProgressing"
+	serviceAccountControllerDegradedCondition    = "ServiceAccountControllerDegraded"
+	roleBindingControllerProgressingCondition    = "RoleBindingControllerProgressing"
+	roleBindingControllerDegradedCondition       = "RoleBindingControllerDegraded"
+	agentTokenControllerProgressingCondition     = "AgentTokenControllerProgressing"
+	agentTokenControllerDegradedCondition        = "AgentTokenControllerDegraded"
+	statefulSetControllerAvailableCondition      = "StatefulSetControllerAvailable"
+	statefulSetControllerProgressingCondition    = "StatefulSetControllerProgressing"
+	statefulSetControllerDegradedCondition       = "StatefulSetControllerDegraded"
+	serviceControllerProgressingCondition        = "ServiceControllerProgressing"
+	serviceControllerDegradedCondition           = "ServiceControllerDegraded"
+	pdbControllerProgressingCondition            = "PDBControllerProgressing"
+	pdbControllerDegradedCondition               = "PDBControllerDegraded"
+	ingressControllerProgressingCondition        = "IngressControllerProgressing"
+	ingressControllerDegradedCondition           = "IngressControllerDegraded"
+)
diff --git a/pkg/controller/scyllacluster/controller.go b/pkg/controller/scyllacluster/controller.go
index 4db845733..b0f8ea572 100644
--- a/pkg/controller/scyllacluster/controller.go
+++ b/pkg/controller/scyllacluster/controller.go
@@ -49,9 +49,9 @@ const (
 )
 
 var (
-	keyFunc                  = cache.DeletionHandlingMetaNamespaceKeyFunc
-	controllerGVK            = scyllav1.GroupVersion.WithKind("ScyllaCluster")
-	statefulSetControllerGVK = appsv1.SchemeGroupVersion.WithKind("StatefulSet")
+	keyFunc                    = cache.DeletionHandlingMetaNamespaceKeyFunc
+	scyllaClusterControllerGVK = scyllav1.GroupVersion.WithKind("ScyllaCluster")
+	statefulSetControllerGVK   = appsv1.SchemeGroupVersion.WithKind("StatefulSet")
 )
 
 type Controller struct {
@@ -265,7 +265,7 @@ func (scc *Controller) resolveScyllaClusterController(obj metav1.Object) *scylla
 		return nil
 	}
 
-	if controllerRef.Kind != controllerGVK.Kind {
+	if controllerRef.Kind != scyllaClusterControllerGVK.Kind {
 		return nil
 	}
 
@@ -457,13 +457,13 @@ func (scc *Controller) deleteSecret(obj interface{}) {
 	scc.enqueueOwner(secret)
 }
 
-func (sac *Controller) addServiceAccount(obj interface{}) {
+func (scc *Controller) addServiceAccount(obj interface{}) {
 	sa := obj.(*corev1.ServiceAccount)
 	klog.V(4).InfoS("Observed addition of ServiceAccount", "ServiceAccount", klog.KObj(sa))
-	sac.enqueueOwner(sa)
+	scc.enqueueOwner(sa)
 }
 
-func (sac *Controller) updateServiceAccount(old, cur interface{}) {
+func (scc *Controller) updateServiceAccount(old, cur interface{}) {
 	oldSA := old.(*corev1.ServiceAccount)
 	currentSA := cur.(*corev1.ServiceAccount)
 
@@ -473,17 +473,17 @@ func (sac *Controller) updateServiceAccount(old, cur interface{}) {
 			utilruntime.HandleError(fmt.Errorf("couldn't get key for object %#v: %v", oldSA, err))
 			return
 		}
-		sac.deleteServiceAccount(cache.DeletedFinalStateUnknown{
+		scc.deleteServiceAccount(cache.DeletedFinalStateUnknown{
 			Key: key,
 			Obj: oldSA,
 		})
 	}
 
 	klog.V(4).InfoS("Observed update of ServiceAccount", "ServiceAccount", klog.KObj(oldSA))
-	sac.enqueueOwner(currentSA)
+	scc.enqueueOwner(currentSA)
 }
 
-func (sac *Controller) deleteServiceAccount(obj interface{}) {
+func (scc *Controller) deleteServiceAccount(obj interface{}) {
 	svc, ok := obj.(*corev1.ServiceAccount)
 	if !ok {
 		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
@@ -498,16 +498,16 @@ func (sac *Controller) deleteServiceAccount(obj interface{}) {
 		}
 	}
 	klog.V(4).InfoS("Observed deletion of ServiceAccount", "ServiceAccount", klog.KObj(svc))
-	sac.enqueueOwner(svc)
+	scc.enqueueOwner(svc)
 }
 
-func (sac *Controller) addRoleBinding(obj interface{}) {
+func (scc *Controller) addRoleBinding(obj interface{}) {
 	roleBinding := obj.(*rbacv1.RoleBinding)
 	klog.V(4).InfoS("Observed addition of RoleBinding", "RoleBinding", klog.KObj(roleBinding))
-	sac.enqueueOwner(roleBinding)
+	scc.enqueueOwner(roleBinding)
 }
 
-func (sac *Controller) updateRoleBinding(old, cur interface{}) {
+func (scc *Controller) updateRoleBinding(old, cur interface{}) {
 	oldRoleBinding := old.(*rbacv1.RoleBinding)
 	currentRoleBinding := cur.(*rbacv1.RoleBinding)
 
@@ -517,17 +517,17 @@ func (sac *Controller) updateRoleBinding(old, cur interface{}) {
 			utilruntime.HandleError(fmt.Errorf("couldn't get key for object %#v: %v", oldRoleBinding, err))
 			return
 		}
-		sac.deleteRoleBinding(cache.DeletedFinalStateUnknown{
+		scc.deleteRoleBinding(cache.DeletedFinalStateUnknown{
 			Key: key,
 			Obj: oldRoleBinding,
 		})
 	}
 
 	klog.V(4).InfoS("Observed update of RoleBinding", "RoleBinding", klog.KObj(oldRoleBinding))
-	sac.enqueueOwner(currentRoleBinding)
+	scc.enqueueOwner(currentRoleBinding)
 }
 
-func (sac *Controller) deleteRoleBinding(obj interface{}) {
+func (scc *Controller) deleteRoleBinding(obj interface{}) {
 	roleBinding, ok := obj.(*rbacv1.RoleBinding)
 	if !ok {
 		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
@@ -542,7 +542,7 @@ func (sac *Controller) deleteRoleBinding(obj interface{}) {
 		}
 	}
 	klog.V(4).InfoS("Observed deletion of RoleBinding", "RoleBinding", klog.KObj(roleBinding))
-	sac.enqueueOwner(roleBinding)
+	scc.enqueueOwner(roleBinding)
 }
 
 func (scc *Controller) addPod(obj interface{}) {
diff --git a/pkg/controller/scyllacluster/resource.go b/pkg/controller/scyllacluster/resource.go
index 941ccd117..5fc9a20e4 100644
--- a/pkg/controller/scyllacluster/resource.go
+++ b/pkg/controller/scyllacluster/resource.go
@@ -50,7 +50,7 @@ func IdentityService(c *scyllav1.ScyllaCluster) *corev1.Service {
 			Namespace: c.Namespace,
 			Labels:    labels,
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(c, controllerGVK),
+				*metav1.NewControllerRef(c, scyllaClusterControllerGVK),
 			},
 		},
 		Spec: corev1.ServiceSpec{
@@ -99,7 +99,7 @@ func MemberService(sc *scyllav1.ScyllaCluster, rackName, name string, oldService
 			Name:      name,
 			Namespace: sc.Namespace,
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(sc, controllerGVK),
+				*metav1.NewControllerRef(sc, scyllaClusterControllerGVK),
 			},
 			Labels: labels,
 		},
@@ -199,7 +199,7 @@ func StatefulSetForRack(r scyllav1.RackSpec, c *scyllav1.ScyllaCluster, existing
 			Namespace: c.Namespace,
 			Labels:    rackLabels,
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(c, controllerGVK),
+				*metav1.NewControllerRef(c, scyllaClusterControllerGVK),
 			},
 		},
 		Spec: appsv1.StatefulSetSpec{
@@ -624,7 +624,7 @@ func MakePodDisruptionBudget(c *scyllav1.ScyllaCluster) *policyv1.PodDisruptionB
 			Name:      naming.PodDisruptionBudgetName(c),
 			Namespace: c.Namespace,
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(c, controllerGVK),
+				*metav1.NewControllerRef(c, scyllaClusterControllerGVK),
 			},
 			Labels: naming.ClusterLabels(c),
 		},
@@ -699,7 +699,7 @@ func MakeIngresses(c *scyllav1.ScyllaCluster, services map[string]*corev1.Servic
 					Labels:      labels,
 					Annotations: ip.ingressOptions.Annotations,
 					OwnerReferences: []metav1.OwnerReference{
-						*metav1.NewControllerRef(c, controllerGVK),
+						*metav1.NewControllerRef(c, scyllaClusterControllerGVK),
 					},
 				},
 				Spec: networkingv1.IngressSpec{
@@ -761,7 +761,7 @@ func MakeAgentAuthTokenSecret(c *scyllav1.ScyllaCluster, authToken string) (*cor
 			Name:      naming.AgentAuthTokenSecretName(c.Name),
 			Namespace: c.Namespace,
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(c, controllerGVK),
+				*metav1.NewControllerRef(c, scyllaClusterControllerGVK),
 			},
 			Labels: naming.ClusterLabels(c),
 		},
@@ -793,7 +793,7 @@ func MakeServiceAccount(sc *scyllav1.ScyllaCluster) *corev1.ServiceAccount {
 			Name:      naming.MemberServiceAccountNameForScyllaCluster(sc.Name),
 			Namespace: sc.Namespace,
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(sc, controllerGVK),
+				*metav1.NewControllerRef(sc, scyllaClusterControllerGVK),
 			},
 			Labels: naming.ClusterLabels(sc),
 		},
@@ -807,7 +807,7 @@ func MakeRoleBinding(sc *scyllav1.ScyllaCluster) *rbacv1.RoleBinding {
 			Name:      saName,
 			Namespace: sc.Namespace,
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(sc, controllerGVK),
+				*metav1.NewControllerRef(sc, scyllaClusterControllerGVK),
 			},
 			Labels: naming.ClusterLabels(sc),
 		},
diff --git a/pkg/controller/scyllacluster/sync.go b/pkg/controller/scyllacluster/sync.go
index 0517c8827..0d37f1241 100644
--- a/pkg/controller/scyllacluster/sync.go
+++ b/pkg/controller/scyllacluster/sync.go
@@ -6,7 +6,9 @@ import (
 	"time"
 
 	scyllav1 "github.com/scylladb/scylla-operator/pkg/api/scylla/v1"
+	"github.com/scylladb/scylla-operator/pkg/controllerhelpers"
 	"github.com/scylladb/scylla-operator/pkg/controllertools"
+	"github.com/scylladb/scylla-operator/pkg/internalapi"
 	"github.com/scylladb/scylla-operator/pkg/naming"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -14,6 +16,7 @@ import (
 	policyv1 "k8s.io/api/policy/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
@@ -54,7 +57,7 @@ func (scc *Controller) getStatefulSets(ctx context.Context, sc *scyllav1.ScyllaC
 	cm := controllertools.NewStatefulSetControllerRefManager(
 		ctx,
 		sc,
-		controllerGVK,
+		scyllaClusterControllerGVK,
 		selector,
 		canAdoptFunc,
 		controllertools.RealStatefulSetControl{
@@ -98,7 +101,7 @@ func (scc *Controller) getServices(ctx context.Context, sc *scyllav1.ScyllaClust
 	cm := controllertools.NewServiceControllerRefManager(
 		ctx,
 		sc,
-		controllerGVK,
+		scyllaClusterControllerGVK,
 		selector,
 		canAdoptFunc,
 		controllertools.RealServiceControl{
@@ -142,7 +145,7 @@ func (scc *Controller) getSecrets(ctx context.Context, sc *scyllav1.ScyllaCluste
 	cm := controllertools.NewSecretControllerRefManager(
 		ctx,
 		sc,
-		controllerGVK,
+		scyllaClusterControllerGVK,
 		selector,
 		canAdoptFunc,
 		controllertools.RealSecretControl{
@@ -153,10 +156,10 @@ func (scc *Controller) getSecrets(ctx context.Context, sc *scyllav1.ScyllaCluste
 	return cm.ClaimSecrets(secrets)
 }
 
-func (sac *Controller) getServiceAccounts(ctx context.Context, sc *scyllav1.ScyllaCluster) (map[string]*corev1.ServiceAccount, error) {
+func (scc *Controller) getServiceAccounts(ctx context.Context, sc *scyllav1.ScyllaCluster) (map[string]*corev1.ServiceAccount, error) {
 	// List all ServiceAccounts to find even those that no longer match our selector.
 	// They will be orphaned in ClaimServiceAccount().
-	serviceAccounts, err := sac.serviceAccountLister.ServiceAccounts(sc.Namespace).List(labels.Everything())
+	serviceAccounts, err := scc.serviceAccountLister.ServiceAccounts(sc.Namespace).List(labels.Everything())
 	if err != nil {
 		return nil, err
 	}
@@ -168,7 +171,7 @@ func (sac *Controller) getServiceAccounts(ctx context.Context, sc *scyllav1.Scyl
 	// If any adoptions are attempted, we should first recheck for deletion with
 	// an uncached quorum read sometime after listing StatefulSets.
 	canAdoptFunc := func() error {
-		fresh, err := sac.scyllaClient.ScyllaClusters(sc.Namespace).Get(ctx, sc.Name, metav1.GetOptions{})
+		fresh, err := scc.scyllaClient.ScyllaClusters(sc.Namespace).Get(ctx, sc.Name, metav1.GetOptions{})
 		if err != nil {
 			return err
 		}
@@ -186,21 +189,21 @@ func (sac *Controller) getServiceAccounts(ctx context.Context, sc *scyllav1.Scyl
 	cm := controllertools.NewServiceAccountControllerRefManager(
 		ctx,
 		sc,
-		controllerGVK,
+		scyllaClusterControllerGVK,
 		selector,
 		canAdoptFunc,
 		controllertools.RealServiceAccountControl{
-			KubeClient: sac.kubeClient,
-			Recorder:   sac.eventRecorder,
+			KubeClient: scc.kubeClient,
+			Recorder:   scc.eventRecorder,
 		},
 	)
 	return cm.ClaimServiceAccounts(serviceAccounts)
 }
 
-func (sac *Controller) getRoleBindings(ctx context.Context, sc *scyllav1.ScyllaCluster) (map[string]*rbacv1.RoleBinding, error) {
+func (scc *Controller) getRoleBindings(ctx context.Context, sc *scyllav1.ScyllaCluster) (map[string]*rbacv1.RoleBinding, error) {
 	// List all RoleBindings to find even those that no longer match our selector.
 	// They will be orphaned in ClaimRoleBindings().
-	roleBindings, err := sac.roleBindingLister.RoleBindings(sc.Namespace).List(labels.Everything())
+	roleBindings, err := scc.roleBindingLister.RoleBindings(sc.Namespace).List(labels.Everything())
 	if err != nil {
 		return nil, err
 	}
@@ -212,7 +215,7 @@ func (sac *Controller) getRoleBindings(ctx context.Context, sc *scyllav1.ScyllaC
 	// If any adoptions are attempted, we should first recheck for deletion with
 	// an uncached quorum read sometime after listing RoleBindings.
 	canAdoptFunc := func() error {
-		fresh, err := sac.scyllaClient.ScyllaClusters(sc.Namespace).Get(ctx, sc.Name, metav1.GetOptions{})
+		fresh, err := scc.scyllaClient.ScyllaClusters(sc.Namespace).Get(ctx, sc.Name, metav1.GetOptions{})
 		if err != nil {
 			return err
 		}
@@ -230,12 +233,12 @@ func (sac *Controller) getRoleBindings(ctx context.Context, sc *scyllav1.ScyllaC
 	cm := controllertools.NewRoleBindingControllerRefManager(
 		ctx,
 		sc,
-		controllerGVK,
+		scyllaClusterControllerGVK,
 		selector,
 		canAdoptFunc,
 		controllertools.RealRoleBindingControl{
-			KubeClient: sac.kubeClient,
-			Recorder:   sac.eventRecorder,
+			KubeClient: scc.kubeClient,
+			Recorder:   scc.eventRecorder,
 		},
 	)
 	return cm.ClaimRoleBindings(roleBindings)
@@ -274,7 +277,7 @@ func (scc *Controller) getPDBs(ctx context.Context, sc *scyllav1.ScyllaCluster)
 	cm := controllertools.NewPodDisruptionBudgetControllerRefManager(
 		ctx,
 		sc,
-		controllerGVK,
+		scyllaClusterControllerGVK,
 		selector,
 		canAdoptFunc,
 		controllertools.RealPodDisruptionBudgetControl{
@@ -318,7 +321,7 @@ func (scc *Controller) getIngresses(ctx context.Context, sc *scyllav1.ScyllaClus
 	cm := controllertools.NewIngressControllerRefManager(
 		ctx,
 		sc,
-		controllerGVK,
+		scyllaClusterControllerGVK,
 		selector,
 		canAdoptFunc,
 		controllertools.RealIngressControl{
@@ -329,6 +332,31 @@ func (scc *Controller) getIngresses(ctx context.Context, sc *scyllav1.ScyllaClus
 	return cm.ClaimIngresss(ingresses)
 }
 
+func runSync(conditions *[]metav1.Condition, progressingConditionType, degradedCondType string, observedGeneration int64, syncFn func() ([]metav1.Condition, error)) error {
+	progressingConditions, err := syncFn()
+	controllerhelpers.SetStatusConditionFromError(conditions, err, degradedCondType, observedGeneration)
+	if err != nil {
+		return err
+	}
+
+	progressingCondition, err := controllerhelpers.AggregateStatusConditions(
+		progressingConditions,
+		metav1.Condition{
+			Type:               progressingConditionType,
+			Status:             metav1.ConditionFalse,
+			Reason:             internalapi.AsExpectedReason,
+			Message:            "",
+			ObservedGeneration: observedGeneration,
+		},
+	)
+	if err != nil {
+		return fmt.Errorf("can't aggregate progressing conditions %q: %w", progressingConditionType, err)
+	}
+	apimeta.SetStatusCondition(conditions, progressingCondition)
+
+	return nil
+}
+
 func (scc *Controller) sync(ctx context.Context, key string) error {
 	namespace, name, err := cache.SplitMetaNamespaceKey(key)
 	if err != nil {
@@ -394,46 +422,148 @@ func (scc *Controller) sync(ctx context.Context, key string) error {
 
 	var errs []error
 
-	err = scc.syncServiceAccounts(ctx, sc, serviceAccounts)
+	err = runSync(
+		&status.Conditions,
+		serviceAccountControllerProgressingCondition,
+		serviceAccountControllerDegradedCondition,
+		sc.Generation,
+		func() ([]metav1.Condition, error) {
+			return scc.syncServiceAccounts(ctx, sc, serviceAccounts)
+		},
+	)
 	if err != nil {
-		errs = append(errs, fmt.Errorf("can't sync serviceaccounts: %w", err))
-		// TODO: Set degraded condition
+		errs = append(errs, fmt.Errorf("can't sync service accounts: %w", err))
 	}
 
-	err = scc.syncRoleBindings(ctx, sc, roleBindings)
+	err = runSync(
+		&status.Conditions,
+		roleBindingControllerProgressingCondition,
+		roleBindingControllerDegradedCondition,
+		sc.Generation,
+		func() ([]metav1.Condition, error) {
+			return scc.syncRoleBindings(ctx, sc, roleBindings)
+		},
+	)
 	if err != nil {
-		errs = append(errs, fmt.Errorf("can't sync rolebindings: %w", err))
-		// TODO: Set degraded condition
+		errs = append(errs, fmt.Errorf("can't sync role bindings: %w", err))
 	}
 
-	status, err = scc.syncAgentToken(ctx, sc, status, secretMap)
+	err = runSync(
+		&status.Conditions,
+		agentTokenControllerProgressingCondition,
+		agentTokenControllerDegradedCondition,
+		sc.Generation,
+		func() ([]metav1.Condition, error) {
+			return scc.syncAgentToken(ctx, sc, secretMap)
+		},
+	)
 	if err != nil {
-		errs = append(errs, err)
-		// TODO: Set degraded condition
+		errs = append(errs, fmt.Errorf("can't sync agent token: %w", err))
 	}
 
-	status, err = scc.syncStatefulSets(ctx, key, sc, status, statefulSetMap, serviceMap)
+	err = runSync(
+		&status.Conditions,
+		statefulSetControllerProgressingCondition,
+		statefulSetControllerDegradedCondition,
+		sc.Generation,
+		func() ([]metav1.Condition, error) {
+			return scc.syncStatefulSets(ctx, key, sc, status, statefulSetMap, serviceMap)
+		},
+	)
 	if err != nil {
-		errs = append(errs, err)
-		// TODO: Set degraded condition
+		errs = append(errs, fmt.Errorf("can't sync stateful sets: %w", err))
+	}
+	// Ideally, this would be projected in calculateStatus but because we are updating the status based on applied
+	// StatefulSets, the rack status can change afterwards. Overtime we should consider adding a status.progressing
+	// field (to allow determining cluster status without conditions) and wait for the status to be updated
+	// in a single place, on the next resync.
+	scc.setStatefulSetsAvailableStatusCondition(sc, status)
+
+	err = runSync(
+		&status.Conditions,
+		serviceControllerProgressingCondition,
+		serviceControllerDegradedCondition,
+		sc.Generation,
+		func() ([]metav1.Condition, error) {
+			return scc.syncServices(ctx, sc, status, serviceMap, statefulSetMap)
+		},
+	)
+	if err != nil {
+		errs = append(errs, fmt.Errorf("can't sync services: %w", err))
 	}
 
-	status, err = scc.syncServices(ctx, sc, status, serviceMap, statefulSetMap)
+	err = runSync(
+		&status.Conditions,
+		pdbControllerProgressingCondition,
+		pdbControllerDegradedCondition,
+		sc.Generation,
+		func() ([]metav1.Condition, error) {
+			return scc.syncPodDisruptionBudgets(ctx, sc, pdbMap)
+		},
+	)
 	if err != nil {
-		errs = append(errs, err)
-		// TODO: Set degraded condition
+		errs = append(errs, fmt.Errorf("can't sync pdbs: %w", err))
 	}
 
-	status, err = scc.syncPodDisruptionBudgets(ctx, sc, status, pdbMap)
+	err = runSync(
+		&status.Conditions,
+		ingressControllerProgressingCondition,
+		ingressControllerDegradedCondition,
+		sc.Generation,
+		func() ([]metav1.Condition, error) {
+			return scc.syncIngresses(ctx, sc, ingressMap, serviceMap)
+		},
+	)
 	if err != nil {
-		errs = append(errs, err)
-		// TODO: Set degraded condition
+		errs = append(errs, fmt.Errorf("can't sync ingresses: %w", err))
 	}
-	status, err = scc.syncIngresses(ctx, sc, status, ingressMap, serviceMap)
+
+	// Aggregate conditions.
+
+	availableCondition, err := controllerhelpers.AggregateStatusConditions(
+		controllerhelpers.FindStatusConditionsWithSuffix(status.Conditions, scyllav1.AvailableCondition),
+		metav1.Condition{
+			Type:               scyllav1.AvailableCondition,
+			Status:             metav1.ConditionTrue,
+			Reason:             internalapi.AsExpectedReason,
+			Message:            "",
+			ObservedGeneration: sc.Generation,
+		},
+	)
+	if err != nil {
+		return fmt.Errorf("can't aggregate status conditions: %w", err)
+	}
+	apimeta.SetStatusCondition(&status.Conditions, availableCondition)
+
+	progressingCondition, err := controllerhelpers.AggregateStatusConditions(
+		controllerhelpers.FindStatusConditionsWithSuffix(status.Conditions, scyllav1.ProgressingCondition),
+		metav1.Condition{
+			Type:               scyllav1.ProgressingCondition,
+			Status:             metav1.ConditionFalse,
+			Reason:             internalapi.AsExpectedReason,
+			Message:            "",
+			ObservedGeneration: sc.Generation,
+		},
+	)
+	if err != nil {
+		return fmt.Errorf("can't aggregate status conditions: %w", err)
+	}
+	apimeta.SetStatusCondition(&status.Conditions, progressingCondition)
+
+	degradedCondition, err := controllerhelpers.AggregateStatusConditions(
+		controllerhelpers.FindStatusConditionsWithSuffix(status.Conditions, scyllav1.DegradedCondition),
+		metav1.Condition{
+			Type:               scyllav1.DegradedCondition,
+			Status:             metav1.ConditionFalse,
+			Reason:             internalapi.AsExpectedReason,
+			Message:            "",
+			ObservedGeneration: sc.Generation,
+		},
+	)
 	if err != nil {
-		errs = append(errs, err)
-		// TODO: Set degraded condition
+		return fmt.Errorf("can't aggregate status conditions: %w", err)
 	}
+	apimeta.SetStatusCondition(&status.Conditions, degradedCondition)
 
 	err = scc.updateStatus(ctx, sc, status)
 	errs = append(errs, err)
diff --git a/pkg/controller/scyllacluster/sync_agent_config.go b/pkg/controller/scyllacluster/sync_agent_config.go
index 5d973f8c9..85f312da7 100644
--- a/pkg/controller/scyllacluster/sync_agent_config.go
+++ b/pkg/controller/scyllacluster/sync_agent_config.go
@@ -5,10 +5,12 @@ import (
 	"fmt"
 
 	scyllav1 "github.com/scylladb/scylla-operator/pkg/api/scylla/v1"
+	"github.com/scylladb/scylla-operator/pkg/controllerhelpers"
 	"github.com/scylladb/scylla-operator/pkg/helpers"
 	"github.com/scylladb/scylla-operator/pkg/naming"
 	"github.com/scylladb/scylla-operator/pkg/resourceapply"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/rand"
 )
 
@@ -30,9 +32,10 @@ func (scc *Controller) getAgentTokenFromAgentConfig(sc *scyllav1.ScyllaCluster)
 func (scc *Controller) syncAgentToken(
 	ctx context.Context,
 	sc *scyllav1.ScyllaCluster,
-	status *scyllav1.ScyllaClusterStatus,
 	secrets map[string]*corev1.Secret,
-) (*scyllav1.ScyllaClusterStatus, error) {
+) ([]metav1.Condition, error) {
+	var progressingConditions []metav1.Condition
+
 	token, tokenErr := scc.getAgentTokenFromAgentConfig(sc)
 	if tokenErr != nil {
 		tokenErr = fmt.Errorf("can't get agent token: %w", tokenErr)
@@ -49,7 +52,7 @@ func (scc *Controller) syncAgentToken(
 			var err error
 			token, err = helpers.GetAgentAuthTokenFromSecret(tokenSecret)
 			if err != nil {
-				return status, fmt.Errorf("can't read token from secret %q: %w", naming.ObjRef(tokenSecret), err)
+				return progressingConditions, fmt.Errorf("can't read token from secret %q: %w", naming.ObjRef(tokenSecret), err)
 			}
 		}
 	}
@@ -61,14 +64,17 @@ func (scc *Controller) syncAgentToken(
 
 	secret, err := MakeAgentAuthTokenSecret(sc, token)
 	if err != nil {
-		return status, fmt.Errorf("can't make auth token secret: %w", err)
+		return progressingConditions, fmt.Errorf("can't make auth token secret: %w", err)
 	}
 
 	// TODO: Remove forced ownership in v1.5 (#672)
-	_, _, err = resourceapply.ApplySecret(ctx, scc.kubeClient.CoreV1(), scc.secretLister, scc.eventRecorder, secret, true)
+	_, changed, err := resourceapply.ApplySecret(ctx, scc.kubeClient.CoreV1(), scc.secretLister, scc.eventRecorder, secret, true)
+	if changed {
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, agentTokenControllerProgressingCondition, secret, "apply", sc.Generation)
+	}
 	if err != nil {
-		return status, fmt.Errorf("can't apply secret %q: %w", naming.ObjRef(secret), err)
+		return progressingConditions, fmt.Errorf("can't apply secret %q: %w", naming.ObjRef(secret), err)
 	}
 
-	return status, tokenErr
+	return progressingConditions, tokenErr
 }
diff --git a/pkg/controller/scyllacluster/sync_ingress.go b/pkg/controller/scyllacluster/sync_ingress.go
index 1da886057..50c171843 100644
--- a/pkg/controller/scyllacluster/sync_ingress.go
+++ b/pkg/controller/scyllacluster/sync_ingress.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	scyllav1 "github.com/scylladb/scylla-operator/pkg/api/scylla/v1"
+	"github.com/scylladb/scylla-operator/pkg/controllerhelpers"
 	"github.com/scylladb/scylla-operator/pkg/resourceapply"
 	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
@@ -15,11 +16,11 @@ import (
 func (scc *Controller) syncIngresses(
 	ctx context.Context,
 	sc *scyllav1.ScyllaCluster,
-	status *scyllav1.ScyllaClusterStatus,
 	ingresses map[string]*networkingv1.Ingress,
 	services map[string]*corev1.Service,
-) (*scyllav1.ScyllaClusterStatus, error) {
+) ([]metav1.Condition, error) {
 	var err error
+	var progressingConditions []metav1.Condition
 
 	requiredIngresses := MakeIngresses(sc, services)
 
@@ -42,6 +43,7 @@ func (scc *Controller) syncIngresses(
 		}
 
 		propagationPolicy := metav1.DeletePropagationBackground
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, ingressControllerProgressingCondition, ingress, "delete", sc.Generation)
 		err = scc.kubeClient.NetworkingV1().Ingresses(ingress.Namespace).Delete(ctx, ingress.Name, metav1.DeleteOptions{
 			Preconditions: &metav1.Preconditions{
 				UID: &ingress.UID,
@@ -52,15 +54,18 @@ func (scc *Controller) syncIngresses(
 	}
 	err = utilerrors.NewAggregate(deletionErrors)
 	if err != nil {
-		return status, fmt.Errorf("can't delete ingress(s): %w", err)
+		return progressingConditions, fmt.Errorf("can't delete ingress(s): %w", err)
 	}
 
 	for _, requiredIngress := range requiredIngresses {
-		_, _, err = resourceapply.ApplyIngress(ctx, scc.kubeClient.NetworkingV1(), scc.ingressLister, scc.eventRecorder, requiredIngress)
+		_, changed, err := resourceapply.ApplyIngress(ctx, scc.kubeClient.NetworkingV1(), scc.ingressLister, scc.eventRecorder, requiredIngress)
+		if changed {
+			controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, ingressControllerProgressingCondition, requiredIngress, "apply", sc.Generation)
+		}
 		if err != nil {
-			return status, fmt.Errorf("can't apply ingress: %w", err)
+			return progressingConditions, fmt.Errorf("can't apply ingress: %w", err)
 		}
 	}
 
-	return status, nil
+	return progressingConditions, nil
 }
diff --git a/pkg/controller/scyllacluster/sync_pdb.go b/pkg/controller/scyllacluster/sync_pdb.go
index c27f31a52..43d10fd2c 100644
--- a/pkg/controller/scyllacluster/sync_pdb.go
+++ b/pkg/controller/scyllacluster/sync_pdb.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	scyllav1 "github.com/scylladb/scylla-operator/pkg/api/scylla/v1"
+	"github.com/scylladb/scylla-operator/pkg/controllerhelpers"
 	"github.com/scylladb/scylla-operator/pkg/resourceapply"
 	policyv1 "k8s.io/api/policy/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -14,10 +15,10 @@ import (
 func (scc *Controller) syncPodDisruptionBudgets(
 	ctx context.Context,
 	sc *scyllav1.ScyllaCluster,
-	status *scyllav1.ScyllaClusterStatus,
 	pdbs map[string]*policyv1.PodDisruptionBudget,
-) (*scyllav1.ScyllaClusterStatus, error) {
+) ([]metav1.Condition, error) {
 	var err error
+	var progressingConditions []metav1.Condition
 
 	requiredPDB := MakePodDisruptionBudget(sc)
 
@@ -34,6 +35,7 @@ func (scc *Controller) syncPodDisruptionBudgets(
 		}
 
 		propagationPolicy := metav1.DeletePropagationBackground
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, pdbControllerProgressingCondition, pdb, "delete", sc.Generation)
 		err = scc.kubeClient.PolicyV1().PodDisruptionBudgets(pdb.Namespace).Delete(ctx, pdb.Name, metav1.DeleteOptions{
 			Preconditions: &metav1.Preconditions{
 				UID: &pdb.UID,
@@ -44,14 +46,17 @@ func (scc *Controller) syncPodDisruptionBudgets(
 	}
 	err = utilerrors.NewAggregate(deletionErrors)
 	if err != nil {
-		return status, fmt.Errorf("can't delete pdb(s): %w", err)
+		return progressingConditions, fmt.Errorf("can't delete pdb(s): %w", err)
 	}
 
 	// TODO: Remove forced ownership in v1.5 (#672)
-	_, _, err = resourceapply.ApplyPodDisruptionBudget(ctx, scc.kubeClient.PolicyV1(), scc.pdbLister, scc.eventRecorder, requiredPDB, true)
+	_, changed, err := resourceapply.ApplyPodDisruptionBudget(ctx, scc.kubeClient.PolicyV1(), scc.pdbLister, scc.eventRecorder, requiredPDB, true)
+	if changed {
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, pdbControllerProgressingCondition, requiredPDB, "apply", sc.Generation)
+	}
 	if err != nil {
-		return status, fmt.Errorf("can't apply pdb: %w", err)
+		return progressingConditions, fmt.Errorf("can't apply pdb: %w", err)
 	}
 
-	return status, nil
+	return progressingConditions, nil
 }
diff --git a/pkg/controller/scyllacluster/sync_rolebindings.go b/pkg/controller/scyllacluster/sync_rolebindings.go
index 79614b063..7f4137bcf 100644
--- a/pkg/controller/scyllacluster/sync_rolebindings.go
+++ b/pkg/controller/scyllacluster/sync_rolebindings.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	scyllav1 "github.com/scylladb/scylla-operator/pkg/api/scylla/v1"
+	"github.com/scylladb/scylla-operator/pkg/controllerhelpers"
 	"github.com/scylladb/scylla-operator/pkg/resourceapply"
 	rbacv1 "k8s.io/api/rbac/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -15,8 +16,9 @@ func (scc *Controller) syncRoleBindings(
 	ctx context.Context,
 	sc *scyllav1.ScyllaCluster,
 	roleBindings map[string]*rbacv1.RoleBinding,
-) error {
+) ([]metav1.Condition, error) {
 	var err error
+	var progressingConditions []metav1.Condition
 
 	requiredRoleBinding := MakeRoleBinding(sc)
 
@@ -33,6 +35,7 @@ func (scc *Controller) syncRoleBindings(
 		}
 
 		propagationPolicy := metav1.DeletePropagationBackground
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, roleBindingControllerProgressingCondition, rb, "delete", sc.Generation)
 		err = scc.kubeClient.RbacV1().RoleBindings(rb.Namespace).Delete(ctx, rb.Name, metav1.DeleteOptions{
 			Preconditions: &metav1.Preconditions{
 				UID: &rb.UID,
@@ -43,13 +46,16 @@ func (scc *Controller) syncRoleBindings(
 	}
 	err = utilerrors.NewAggregate(deletionErrors)
 	if err != nil {
-		return fmt.Errorf("can't delete role binding(s): %w", err)
+		return progressingConditions, fmt.Errorf("can't delete role binding(s): %w", err)
 	}
 
-	_, _, err = resourceapply.ApplyRoleBinding(ctx, scc.kubeClient.RbacV1(), scc.roleBindingLister, scc.eventRecorder, requiredRoleBinding, true, false)
+	_, changed, err := resourceapply.ApplyRoleBinding(ctx, scc.kubeClient.RbacV1(), scc.roleBindingLister, scc.eventRecorder, requiredRoleBinding, true, false)
+	if changed {
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, roleBindingControllerProgressingCondition, requiredRoleBinding, "apply", sc.Generation)
+	}
 	if err != nil {
-		return fmt.Errorf("can't apply role binding: %w", err)
+		return progressingConditions, fmt.Errorf("can't apply role binding: %w", err)
 	}
 
-	return nil
+	return progressingConditions, nil
 }
diff --git a/pkg/controller/scyllacluster/sync_serviceaccounts.go b/pkg/controller/scyllacluster/sync_serviceaccounts.go
index d98ff3505..bda7d78e5 100644
--- a/pkg/controller/scyllacluster/sync_serviceaccounts.go
+++ b/pkg/controller/scyllacluster/sync_serviceaccounts.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	scyllav1 "github.com/scylladb/scylla-operator/pkg/api/scylla/v1"
+	"github.com/scylladb/scylla-operator/pkg/controllerhelpers"
 	"github.com/scylladb/scylla-operator/pkg/resourceapply"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -15,8 +16,9 @@ func (scc *Controller) syncServiceAccounts(
 	ctx context.Context,
 	sc *scyllav1.ScyllaCluster,
 	serviceAccounts map[string]*corev1.ServiceAccount,
-) error {
+) ([]metav1.Condition, error) {
 	var err error
+	var progressingConditions []metav1.Condition
 
 	requiredServiceAccount := MakeServiceAccount(sc)
 
@@ -33,6 +35,7 @@ func (scc *Controller) syncServiceAccounts(
 		}
 
 		propagationPolicy := metav1.DeletePropagationBackground
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceAccountControllerProgressingCondition, sa, "delete", sc.Generation)
 		err = scc.kubeClient.CoreV1().ServiceAccounts(sa.Namespace).Delete(ctx, sa.Name, metav1.DeleteOptions{
 			Preconditions: &metav1.Preconditions{
 				UID: &sa.UID,
@@ -43,13 +46,16 @@ func (scc *Controller) syncServiceAccounts(
 	}
 	err = utilerrors.NewAggregate(deletionErrors)
 	if err != nil {
-		return fmt.Errorf("can't delete service account(s): %w", err)
+		return progressingConditions, fmt.Errorf("can't delete service account(s): %w", err)
 	}
 
-	_, _, err = resourceapply.ApplyServiceAccount(ctx, scc.kubeClient.CoreV1(), scc.serviceAccountLister, scc.eventRecorder, requiredServiceAccount, true, false)
+	_, changed, err := resourceapply.ApplyServiceAccount(ctx, scc.kubeClient.CoreV1(), scc.serviceAccountLister, scc.eventRecorder, requiredServiceAccount, true, false)
+	if changed {
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceAccountControllerProgressingCondition, requiredServiceAccount, "apply", sc.Generation)
+	}
 	if err != nil {
-		return fmt.Errorf("can't apply service account: %w", err)
+		return progressingConditions, fmt.Errorf("can't apply service account: %w", err)
 	}
 
-	return nil
+	return progressingConditions, nil
 }
diff --git a/pkg/controller/scyllacluster/sync_services.go b/pkg/controller/scyllacluster/sync_services.go
index 01f2e0d89..211817c31 100644
--- a/pkg/controller/scyllacluster/sync_services.go
+++ b/pkg/controller/scyllacluster/sync_services.go
@@ -45,8 +45,9 @@ func (scc *Controller) pruneServices(
 	requiredServices []*corev1.Service,
 	services map[string]*corev1.Service,
 	statefulSets map[string]*appsv1.StatefulSet,
-) error {
+) ([]metav1.Condition, error) {
 	var errs []error
+	var progressingConditions []metav1.Condition
 	for _, svc := range services {
 		if svc.DeletionTimestamp != nil {
 			continue
@@ -87,12 +88,26 @@ func (scc *Controller) pruneServices(
 			continue
 		}
 		if int32(svcOrdinal) < *sts.Spec.Replicas {
+			progressingConditions = append(progressingConditions, metav1.Condition{
+				Type:               serviceControllerProgressingCondition,
+				Status:             metav1.ConditionTrue,
+				Reason:             "WaitingForAnotherService",
+				Message:            fmt.Sprintf("Service %q is waiting for another service to be decommissioned first.", naming.ObjRef(svc)),
+				ObservedGeneration: sc.Generation,
+			})
 			continue
 		}
 
 		// Do not delete services that weren't properly decommissioned.
 		if svc.Labels[naming.DecommissionedLabel] != naming.LabelValueTrue {
 			klog.Warningf("Refusing to cleanup service %s/%s whose member wasn't decommissioned.", svc.Namespace, svc.Name)
+			progressingConditions = append(progressingConditions, metav1.Condition{
+				Type:               serviceControllerProgressingCondition,
+				Status:             metav1.ConditionTrue,
+				Reason:             "WaitingForServiceDecommission",
+				Message:            fmt.Sprintf("Waiting for service %q to be fully decommissioned", naming.ObjRef(svc)),
+				ObservedGeneration: sc.Generation,
+			})
 			continue
 		}
 
@@ -107,17 +122,32 @@ func (scc *Controller) pruneServices(
 
 			if freshSvc.UID != svc.UID {
 				klog.V(2).InfoS("Stale caches, won't delete the pvc because the service UIDs don't match.", "Service", klog.KObj(svc))
+				progressingConditions = append(progressingConditions, metav1.Condition{
+					Type:               serviceControllerProgressingCondition,
+					Status:             metav1.ConditionTrue,
+					Reason:             "WaitingForCaches",
+					Message:            fmt.Sprintf("Service %q is outdated because live UID is different. Waiting on service caches to catch up.", naming.ObjRef(svc)),
+					ObservedGeneration: sc.Generation,
+				})
 				continue
 			}
 
 			if freshSvc.Labels[naming.DecommissionedLabel] != naming.LabelValueTrue {
 				klog.V(2).InfoS("Stale caches, won't delete the pvc because the service is no longer decommissioned.", "Service", klog.KObj(svc))
+				progressingConditions = append(progressingConditions, metav1.Condition{
+					Type:               serviceControllerProgressingCondition,
+					Status:             metav1.ConditionTrue,
+					Reason:             "WaitingForCaches",
+					Message:            fmt.Sprintf("Service %q is outdated because the service is no longed decommisioned. Waiting on service caches to catch up.", naming.ObjRef(svc)),
+					ObservedGeneration: sc.Generation,
+				})
 				continue
 			}
 		}
 
 		backgroundPropagationPolicy := metav1.DeletePropagationBackground
 
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceControllerProgressingCondition, &corev1.PersistentVolumeClaim{}, "delete", sc.Generation)
 		pvcName := naming.PVCNameForService(svc.Name)
 		err = scc.kubeClient.CoreV1().PersistentVolumeClaims(svc.Namespace).Delete(ctx, pvcName, metav1.DeleteOptions{
 			PropagationPolicy: &backgroundPropagationPolicy,
@@ -127,6 +157,7 @@ func (scc *Controller) pruneServices(
 			continue
 		}
 
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceControllerProgressingCondition, svc, "delete", sc.Generation)
 		err = scc.kubeClient.CoreV1().Services(svc.Namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{
 			Preconditions: &metav1.Preconditions{
 				UID:             &svc.UID,
@@ -139,7 +170,7 @@ func (scc *Controller) pruneServices(
 			continue
 		}
 	}
-	return utilerrors.NewAggregate(errs)
+	return progressingConditions, utilerrors.NewAggregate(errs)
 }
 
 func (scc *Controller) syncServices(
@@ -148,23 +179,26 @@ func (scc *Controller) syncServices(
 	status *scyllav1.ScyllaClusterStatus,
 	services map[string]*corev1.Service,
 	statefulSets map[string]*appsv1.StatefulSet,
-) (*scyllav1.ScyllaClusterStatus, error) {
+) ([]metav1.Condition, error) {
 	var err error
 
 	requiredServices := scc.makeServices(sc, services)
 
 	// Delete any excessive Services.
 	// Delete has to be the fist action to avoid getting stuck on quota.
-	err = scc.pruneServices(ctx, sc, requiredServices, services, statefulSets)
+	progressingConditions, err := scc.pruneServices(ctx, sc, requiredServices, services, statefulSets)
 	if err != nil {
-		return status, fmt.Errorf("can't delete Service(s): %w", err)
+		return nil, fmt.Errorf("can't delete Service(s): %w", err)
 	}
 
 	// We need to first propagate ReplaceAddressFirstBoot from status for the new service.
 	for _, svc := range requiredServices {
-		_, _, err = resourceapply.ApplyService(ctx, scc.kubeClient.CoreV1(), scc.serviceLister, scc.eventRecorder, svc, false)
+		_, changed, err := resourceapply.ApplyService(ctx, scc.kubeClient.CoreV1(), scc.serviceLister, scc.eventRecorder, svc, false)
+		if changed {
+			controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceControllerProgressingCondition, svc, "apply", sc.Generation)
+		}
 		if err != nil {
-			return status, err
+			return progressingConditions, err
 		}
 	}
 
@@ -177,7 +211,7 @@ func (scc *Controller) syncServices(
 
 		rackName, ok := svc.Labels[naming.RackNameLabel]
 		if !ok {
-			return status, fmt.Errorf("service %s/%s is missing %q label", svc.Namespace, svc.Name, naming.RackNameLabel)
+			return progressingConditions, fmt.Errorf("service %s/%s is missing %q label", svc.Namespace, svc.Name, naming.RackNameLabel)
 		}
 
 		rackStatus := status.Racks[rackName]
@@ -196,7 +230,14 @@ func (scc *Controller) syncServices(
 				klog.V(2).InfoS("Adding member address to replace address list", "Member", svc.Name, "IP", svc.Spec.ClusterIP, "ReplaceAddresses", status.Racks[rackName].ReplaceAddressFirstBoot)
 
 				// Make sure the address is stored before proceeding further.
-				return status, nil
+				progressingConditions = append(progressingConditions, metav1.Condition{
+					Type:               serviceControllerProgressingCondition,
+					Status:             metav1.ConditionTrue,
+					Reason:             "WaitingToPickUpReplaceAddress",
+					Message:            fmt.Sprintf("Service %q is waiting to pick up stored replace address.", naming.ObjRef(svc)),
+					ObservedGeneration: sc.Generation,
+				})
+				return progressingConditions, nil
 			}
 
 			backgroundPropagationPolicy := metav1.DeletePropagationBackground
@@ -215,6 +256,7 @@ func (scc *Controller) syncServices(
 				"Service", klog.KObj(svc),
 				"PVC", klog.KObj(pvcMeta),
 			)
+			controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceControllerProgressingCondition, pvcMeta, "delete", sc.Generation)
 			err = scc.kubeClient.CoreV1().PersistentVolumeClaims(pvcMeta.Namespace).Delete(ctx, pvcMeta.Name, metav1.DeleteOptions{
 				PropagationPolicy: &backgroundPropagationPolicy,
 			})
@@ -223,7 +265,7 @@ func (scc *Controller) syncServices(
 					klog.V(4).InfoS("PVC not found", "PVC", klog.KObj(pvcMeta))
 				} else {
 					resourceapply.ReportDeleteEvent(scc.eventRecorder, pvcMeta, err)
-					return status, err
+					return progressingConditions, err
 				}
 			}
 			resourceapply.ReportDeleteEvent(scc.eventRecorder, pvcMeta, nil)
@@ -233,6 +275,13 @@ func (scc *Controller) syncServices(
 			// it's presence only from its cache, and never recreates it if it's missing, only when it
 			// creates the pod. This gives StatefulSet controller a chance to see the PVC was deleted
 			// before deleting the pod.
+			progressingConditions = append(progressingConditions, metav1.Condition{
+				Type:               serviceControllerProgressingCondition,
+				Status:             metav1.ConditionTrue,
+				Reason:             "WaitingForStatefulSetToObservePVCDeletion",
+				Message:            "Waiting for StatefulSet controller to observe PVC deletion.",
+				ObservedGeneration: sc.Generation,
+			})
 			time.Sleep(10 * time.Second)
 
 			// Evict the Pod if it exists.
@@ -247,6 +296,7 @@ func (scc *Controller) syncServices(
 				"Service", klog.KObj(svc),
 				"Pod", klog.KObj(podMeta),
 			)
+			controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceControllerProgressingCondition, podMeta, "delete", sc.Generation)
 			// TODO: Revert back to eviction when it's fixed in kubernetes 1.19.z (#732)
 			//       (https://github.com/kubernetes/kubernetes/issues/103970)
 			err = scc.kubeClient.CoreV1().Pods(podMeta.Namespace).Delete(ctx, podMeta.Name, metav1.DeleteOptions{
@@ -257,7 +307,7 @@ func (scc *Controller) syncServices(
 					klog.V(4).InfoS("Pod not found", "Pod", klog.ObjectRef{Namespace: svc.Namespace, Name: svc.Name})
 				} else {
 					resourceapply.ReportDeleteEvent(scc.eventRecorder, podMeta, err)
-					return status, err
+					return progressingConditions, err
 				}
 			}
 			resourceapply.ReportDeleteEvent(scc.eventRecorder, podMeta, nil)
@@ -267,6 +317,7 @@ func (scc *Controller) syncServices(
 				"ScyllaCluster", klog.KObj(sc),
 				"Service", klog.KObj(svc),
 			)
+			controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceControllerProgressingCondition, svc, "delete", sc.Generation)
 			err = scc.kubeClient.CoreV1().Services(svc.Namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{
 				PropagationPolicy: &backgroundPropagationPolicy,
 				Preconditions: &metav1.Preconditions{
@@ -280,7 +331,7 @@ func (scc *Controller) syncServices(
 					klog.V(4).InfoS("Pod not found", "Pod", klog.ObjectRef{Namespace: svc.Namespace, Name: svc.Name})
 				} else {
 					resourceapply.ReportDeleteEvent(scc.eventRecorder, svc, err)
-					return status, err
+					return progressingConditions, err
 				}
 			} else {
 				resourceapply.ReportDeleteEvent(scc.eventRecorder, svc, nil)
@@ -289,7 +340,14 @@ func (scc *Controller) syncServices(
 				//        We can't delete the pod here as it wouldn't retry failures.
 
 				// We have deleted the service. Wait for re-queue.
-				return status, nil
+				progressingConditions = append(progressingConditions, metav1.Condition{
+					Type:               serviceControllerProgressingCondition,
+					Status:             metav1.ConditionTrue,
+					Reason:             "WaitingToObserveDeletedService",
+					Message:            fmt.Sprintf("Waiting to observe service %q as deleted to proceeed with other services.", naming.ObjRef(svc)),
+					ObservedGeneration: sc.Generation,
+				})
+				return progressingConditions, nil
 			}
 		} else {
 			// Member is being replaced. Wait for readiness and clear the replace label.
@@ -297,38 +355,42 @@ func (scc *Controller) syncServices(
 			pod, err := scc.podLister.Pods(svc.Namespace).Get(svc.Name)
 			if err != nil {
 				if !apierrors.IsNotFound(err) {
-					return status, err
+					return progressingConditions, err
 				}
 
 				klog.V(2).InfoS("Pod has not been recreated by the StatefulSet controller yet",
 					"ScyllaCluster", klog.KObj(sc),
 					"Pod", klog.KObj(pod),
 				)
+				progressingConditions = append(progressingConditions, metav1.Condition{
+					Type:               serviceControllerProgressingCondition,
+					Status:             metav1.ConditionTrue,
+					Reason:             "WaitingForPodRecreation",
+					Message:            fmt.Sprintf("Service %q is waiting for StatefulSet controller to recreate pod %q.", naming.ObjRef(svc), naming.ObjRef(pod)),
+					ObservedGeneration: sc.Generation,
+				})
 			} else {
 				sc := scc.resolveScyllaClusterControllerThroughStatefulSet(pod)
 				if sc == nil {
-					klog.ErrorS(nil, "Pod matching our selector is not owned by us.",
-						"ScyllaCluster", klog.KObj(sc),
-						"Pod", klog.KObj(pod),
-					)
-					// User needs to fix the collision manually so there
-					// is no point in returning an error and retrying.
-					return status, nil
+					// User needs to fix the collision manually. We are subscribed to changes
+					// and there is no point in retrying.
+					return progressingConditions, fmt.Errorf("pod %q is not owned by us anymore", naming.ObjRef(pod))
 				}
 
 				// We could still see an old pod in the caches - verify with a live call.
 				podReady, pod, err := controllerhelpers.IsPodReadyWithPositiveLiveCheck(ctx, scc.kubeClient.CoreV1(), pod)
 				if err != nil {
-					return status, err
+					return progressingConditions, err
 				}
 				if podReady {
 					scc.eventRecorder.Eventf(svc, corev1.EventTypeNormal, "FinishedReplacingNode", "New pod %s/%s is ready.", pod.Namespace, pod.Name)
 					svcCopy := svc.DeepCopy()
 					delete(svcCopy.Labels, naming.ReplaceLabel)
+					controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, serviceControllerProgressingCondition, svcCopy, "update", sc.Generation)
 					_, err := scc.kubeClient.CoreV1().Services(svcCopy.Namespace).Update(ctx, svcCopy, metav1.UpdateOptions{})
 					resourceapply.ReportUpdateEvent(scc.eventRecorder, svc, err)
 					if err != nil {
-						return status, err
+						return progressingConditions, err
 					}
 
 					delete(status.Racks[rackName].ReplaceAddressFirstBoot, svc.Name)
@@ -337,10 +399,17 @@ func (scc *Controller) syncServices(
 						"ScyllaCluster", klog.KObj(sc),
 						"Pod", klog.KObj(pod),
 					)
+					progressingConditions = append(progressingConditions, metav1.Condition{
+						Type:               serviceControllerProgressingCondition,
+						Status:             metav1.ConditionTrue,
+						Reason:             "WaitingForPodReadiness",
+						Message:            fmt.Sprintf("Service %q is waiting for Pod %q to become ready.", naming.ObjRef(svc), naming.ObjRef(pod)),
+						ObservedGeneration: sc.Generation,
+					})
 				}
 			}
 		}
 	}
 
-	return status, nil
+	return progressingConditions, nil
 }
diff --git a/pkg/controller/scyllacluster/sync_statefulsets.go b/pkg/controller/scyllacluster/sync_statefulsets.go
index 65ec73c5a..0f9e8cd6c 100644
--- a/pkg/controller/scyllacluster/sync_statefulsets.go
+++ b/pkg/controller/scyllacluster/sync_statefulsets.go
@@ -4,12 +4,14 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 	"time"
 
 	"github.com/blang/semver"
 	scyllav1 "github.com/scylladb/scylla-operator/pkg/api/scylla/v1"
 	"github.com/scylladb/scylla-operator/pkg/controllerhelpers"
 	"github.com/scylladb/scylla-operator/pkg/helpers"
+	"github.com/scylladb/scylla-operator/pkg/internalapi"
 	"github.com/scylladb/scylla-operator/pkg/naming"
 	"github.com/scylladb/scylla-operator/pkg/resourceapply"
 	"github.com/scylladb/scylla-operator/pkg/scyllaclient"
@@ -19,6 +21,7 @@ import (
 	autoscalingv1 "k8s.io/api/autoscaling/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
@@ -329,11 +332,13 @@ func (scc *Controller) afterNodeUpgrade(ctx context.Context, sc *scyllav1.Scylla
 
 func (scc *Controller) pruneStatefulSets(
 	ctx context.Context,
+	sc *scyllav1.ScyllaCluster,
 	status *scyllav1.ScyllaClusterStatus,
 	requiredStatefulSets []*appsv1.StatefulSet,
 	statefulSets map[string]*appsv1.StatefulSet,
-) (*scyllav1.ScyllaClusterStatus, error) {
+) ([]metav1.Condition, error) {
 	var errs []error
+	var progressingConditions []metav1.Condition
 	for _, sts := range statefulSets {
 		if sts.DeletionTimestamp != nil {
 			continue
@@ -352,6 +357,7 @@ func (scc *Controller) pruneStatefulSets(
 		// TODO: Decommission the rack before removal.
 
 		propagationPolicy := metav1.DeletePropagationBackground
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, statefulSetControllerProgressingCondition, sts, "delete", sc.Generation)
 		err := scc.kubeClient.AppsV1().StatefulSets(sts.Namespace).Delete(ctx, sts.Name, metav1.DeleteOptions{
 			Preconditions: &metav1.Preconditions{
 				UID: &sts.UID,
@@ -373,7 +379,7 @@ func (scc *Controller) pruneStatefulSets(
 
 		delete(status.Racks, rackName)
 	}
-	return status, utilerrors.NewAggregate(errs)
+	return progressingConditions, utilerrors.NewAggregate(errs)
 }
 
 // createMissingStatefulSets creates missing StatefulSets.
@@ -385,9 +391,9 @@ func (scc *Controller) createMissingStatefulSets(
 	requiredStatefulSets []*appsv1.StatefulSet,
 	statefulSets map[string]*appsv1.StatefulSet,
 	services map[string]*corev1.Service,
-) (bool, error) {
+) ([]metav1.Condition, error) {
 	var errs []error
-	anyChanged := false
+	var progressingConditions []metav1.Condition
 	for _, req := range requiredStatefulSets {
 		klog.V(4).InfoS("Processing required StatefulSet", "StatefulSet", klog.KObj(req))
 		// Check the adopted set.
@@ -402,7 +408,7 @@ func (scc *Controller) createMissingStatefulSets(
 				continue
 			}
 			if changed {
-				anyChanged = true
+				controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, statefulSetControllerProgressingCondition, req, "apply", sc.Generation)
 
 				rackName, ok := sts.Labels[naming.RackNameLabel]
 				if !ok {
@@ -427,16 +433,23 @@ func (scc *Controller) createMissingStatefulSets(
 		// Wait for the StatefulSet to rollout. Racks can only bootstrap one by one.
 		rolledOut, err := controllerhelpers.IsStatefulSetRolledOut(sts)
 		if err != nil {
-			return false, err
+			return progressingConditions, err
 		}
 
 		if !rolledOut {
 			klog.V(4).InfoS("Waiting for StatefulSet rollout", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(sts))
-			return false, nil
+			progressingConditions = append(progressingConditions, metav1.Condition{
+				Type:               statefulSetControllerProgressingCondition,
+				Status:             metav1.ConditionTrue,
+				Reason:             "WaitingForStatefulSetRollout",
+				Message:            fmt.Sprintf("Waiting for StatefulSet %q to rollout.", naming.ObjRef(req)),
+				ObservedGeneration: sc.Generation,
+			})
+			return progressingConditions, nil
 		}
 	}
 
-	return !anyChanged, utilerrors.NewAggregate(errs)
+	return progressingConditions, utilerrors.NewAggregate(errs)
 }
 
 func (scc *Controller) syncStatefulSets(
@@ -446,8 +459,9 @@ func (scc *Controller) syncStatefulSets(
 	status *scyllav1.ScyllaClusterStatus,
 	statefulSets map[string]*appsv1.StatefulSet,
 	services map[string]*corev1.Service,
-) (*scyllav1.ScyllaClusterStatus, error) {
+) ([]metav1.Condition, error) {
 	var err error
+	var progressingConditions []metav1.Condition
 
 	requiredStatefulSets, err := scc.makeRacks(sc, statefulSets)
 	if err != nil {
@@ -457,27 +471,29 @@ func (scc *Controller) syncStatefulSets(
 			"InvalidRack",
 			fmt.Sprintf("Failed to make rack: %v", err),
 		)
-		return status, err
+		return progressingConditions, err
 	}
 
 	// Delete any excessive StatefulSets.
 	// Delete has to be the first action to avoid getting stuck on quota.
-	status, err = scc.pruneStatefulSets(ctx, status, requiredStatefulSets, statefulSets)
+	pruneProgressingConditions, err := scc.pruneStatefulSets(ctx, sc, status, requiredStatefulSets, statefulSets)
+	progressingConditions = append(progressingConditions, pruneProgressingConditions...)
 	if err != nil {
-		return status, fmt.Errorf("can't delete StatefulSet(s): %w", err)
+		return progressingConditions, fmt.Errorf("can't delete StatefulSet(s): %w", err)
 	}
 
 	// Before any update, make sure all StatefulSets are present.
 	// Create any that are missing.
-	done, err := scc.createMissingStatefulSets(ctx, sc, status, requiredStatefulSets, statefulSets, services)
+	createProgressingConditions, err := scc.createMissingStatefulSets(ctx, sc, status, requiredStatefulSets, statefulSets, services)
+	progressingConditions = append(progressingConditions, createProgressingConditions...)
 	if err != nil {
-		return status, fmt.Errorf("can't create StatefulSet(s): %w", err)
+		return progressingConditions, fmt.Errorf("can't create StatefulSet(s): %w", err)
 	}
-	if !done {
+	if len(createProgressingConditions) > 0 {
 		// Wait for the informers to catch up.
 		// TODO: Add expectations, not to reconcile sooner then we see this new StatefulSet in our caches. (#682)
 		time.Sleep(artificialDelayForCachesToCatchUp)
-		return status, nil
+		return progressingConditions, nil
 	}
 
 	// Scale before the update.
@@ -507,6 +523,13 @@ func (scc *Controller) syncStatefulSets(
 		for _, svc := range rackServices {
 			if svc.Labels[naming.DecommissionedLabel] == naming.LabelValueFalse {
 				klog.V(4).InfoS("Waiting for service to be decommissioned")
+				progressingConditions = append(progressingConditions, metav1.Condition{
+					Type:               statefulSetControllerProgressingCondition,
+					Status:             metav1.ConditionTrue,
+					Reason:             "WaitingForRackServiceDecommission",
+					Message:            fmt.Sprintf("Waiting for rack service %q to decommission.", naming.ObjRef(svc)),
+					ObservedGeneration: sc.Generation,
+				})
 
 				rackName, ok := sts.Labels[naming.RackNameLabel]
 				if ok && len(rackName) != 0 {
@@ -517,7 +540,7 @@ func (scc *Controller) syncStatefulSets(
 					klog.Warningf("Can't set decommissioning condition sts %s/%s because it's missing rack label.", sts.Namespace, sts.Name)
 				}
 
-				return status, nil
+				return progressingConditions, nil
 			}
 		}
 
@@ -533,9 +556,16 @@ func (scc *Controller) syncStatefulSets(
 			lastSvc, ok := rackServices[lastSvcName]
 			if !ok {
 				klog.V(4).InfoS("Missing service", "ScyllaCluster", klog.KObj(sc), "ServiceName", lastSvcName)
+				progressingConditions = append(progressingConditions, metav1.Condition{
+					Type:               statefulSetControllerProgressingCondition,
+					Status:             metav1.ConditionTrue,
+					Reason:             "WaitingForMissingService",
+					Message:            fmt.Sprintf("Statusfulset %q is waiting for service %q to be created", naming.ObjRef(req), lastSvcName),
+					ObservedGeneration: sc.Generation,
+				})
 				// Services are managed in the other loop.
 				// When informers see the new service, will get re-queued.
-				return status, nil
+				return progressingConditions, nil
 			}
 
 			if len(lastSvc.Labels[naming.DecommissionedLabel]) == 0 {
@@ -544,20 +574,22 @@ func (scc *Controller) syncStatefulSets(
 				// TODO: Move this into syncServices so it reconciles properly. This is edge triggered
 				//  and nothing will reconcile the label if something goes wrong or the flow changes.
 				lastSvcCopy.Labels[naming.DecommissionedLabel] = naming.LabelValueFalse
+				controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, statefulSetControllerProgressingCondition, lastSvcCopy, "update", sc.Generation)
 				_, err := scc.kubeClient.CoreV1().Services(lastSvcCopy.Namespace).Update(ctx, lastSvcCopy, metav1.UpdateOptions{})
 				if err != nil {
-					return status, err
+					return progressingConditions, err
 				}
-				return status, nil
+				return progressingConditions, nil
 			}
 		}
 
 		klog.V(2).InfoS("Scaling StatefulSet", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(sts), "CurrentReplicas", *sts.Spec.Replicas, "UpdatedReplicas", scale.Spec.Replicas)
+		controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, statefulSetControllerProgressingCondition, scale, "updateScale", sc.Generation)
 		_, err = scc.kubeClient.AppsV1().StatefulSets(sts.Namespace).UpdateScale(ctx, sts.Name, scale, metav1.UpdateOptions{})
 		if err != nil {
-			return status, fmt.Errorf("can't update scale: %w", err)
+			return progressingConditions, fmt.Errorf("can't update scale: %w", err)
 		}
-		return status, err
+		return progressingConditions, err
 	}
 
 	// TODO: This blocks unstucking by an update.
@@ -568,17 +600,32 @@ func (scc *Controller) syncStatefulSets(
 
 		rolledOut, err := controllerhelpers.IsStatefulSetRolledOut(sts)
 		if err != nil {
-			return status, err
+			return progressingConditions, err
 		}
 
 		if !rolledOut {
 			klog.V(4).InfoS("Waiting for StatefulSet rollout", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(sts))
-			return status, nil
+			progressingConditions = append(progressingConditions, metav1.Condition{
+				Type:               statefulSetControllerProgressingCondition,
+				Status:             metav1.ConditionTrue,
+				Reason:             "WaitingForStatefulSetRollout",
+				Message:            fmt.Sprintf("Waiting for StatefulSet %q to rollout.", naming.ObjRef(req)),
+				ObservedGeneration: sc.Generation,
+			})
+			return progressingConditions, nil
 		}
 	}
 
 	// Run hooks if an upgrade is in progress.
 	if status.Upgrade != nil {
+		progressingConditions = append(progressingConditions, metav1.Condition{
+			Type:               statefulSetControllerProgressingCondition,
+			Status:             metav1.ConditionTrue,
+			Reason:             "RunningUpgradeHooks",
+			Message:            "Running upgrade hooks",
+			ObservedGeneration: sc.Generation,
+		})
+
 		// Isolate the live values in a block to prevent accidental use.
 		{
 			// We could still see an old status. Although hooks are mandated to be reentrant,
@@ -586,14 +633,14 @@ func (scc *Controller) syncStatefulSets(
 			// TODO: Remove the live call when the hooks are migrated to run as Jobs.
 			freshSC, err := scc.scyllaClient.ScyllaClusters(sc.Namespace).Get(ctx, sc.Name, metav1.GetOptions{})
 			if err != nil {
-				return status, err
+				return progressingConditions, err
 			}
 
 			if freshSC.Status.Upgrade == nil ||
 				freshSC.Status.Upgrade.State != status.Upgrade.State {
 				// Wait for requeue.
 				klog.V(2).InfoS("Stale upgrade status, waiting for requeue", "ScyllaCluster", sc)
-				return status, err
+				return progressingConditions, err
 			}
 		}
 
@@ -603,15 +650,15 @@ func (scc *Controller) syncStatefulSets(
 			// TODO: Move the pre-upgrade hook into a Job.
 			done, err := scc.beforeUpgrade(ctx, sc, services)
 			if err != nil {
-				return status, err
+				return progressingConditions, err
 			}
 			if !done {
 				scc.queue.AddAfter(key, 5*time.Second)
-				return status, nil
+				return progressingConditions, nil
 			}
 
 			status.Upgrade.State = string(RolloutInitUpgradePhase)
-			return status, nil
+			return progressingConditions, nil
 
 		case string(RolloutInitUpgradePhase):
 			// Partition all StatefulSet at once to block changes but no Pod update is done yet.
@@ -621,7 +668,7 @@ func (scc *Controller) syncStatefulSets(
 				existing, ok := statefulSets[required.Name]
 				if !ok {
 					// At this point all missing statefulSets should have been created.
-					return status, fmt.Errorf("internal error: can't lookup stateful set %s/%s", required.Namespace, required.Name)
+					return progressingConditions, fmt.Errorf("internal error: can't lookup stateful set %s/%s", required.Namespace, required.Name)
 				}
 				// We are depending on the current values so we need to use optimistic concurrency.
 				// It will make sure we always set the corresponding partition for the scale.
@@ -658,11 +705,11 @@ func (scc *Controller) syncStatefulSets(
 			}
 			err = utilerrors.NewAggregate(errs)
 			if err != nil {
-				return status, err
+				return progressingConditions, err
 			}
 
 			status.Upgrade.State = string(RolloutRunUpgradePhase)
-			return status, nil
+			return progressingConditions, nil
 
 		case string(RolloutRunUpgradePhase):
 			for _, sts := range requiredStatefulSets {
@@ -675,14 +722,14 @@ func (scc *Controller) syncStatefulSets(
 					// they are pretty expensive to run so it's cheaper to recheck the partition with a live call.
 					freshSts, err := scc.kubeClient.AppsV1().StatefulSets(sts.Namespace).Get(ctx, sts.Name, metav1.GetOptions{})
 					if err != nil {
-						return status, err
+						return progressingConditions, err
 					}
 
 					if freshSts.Spec.UpdateStrategy.RollingUpdate == nil ||
 						*freshSts.Spec.UpdateStrategy.RollingUpdate.Partition != partition {
 						// Wait for requeue.
 						klog.V(2).InfoS("Stale StatefulSet partition, waiting for requeue", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(sts))
-						return status, nil
+						return progressingConditions, nil
 					}
 				}
 
@@ -698,7 +745,7 @@ func (scc *Controller) syncStatefulSets(
 					// TODO: Move the post-node-upgrade hook into a Job.
 					err = scc.afterNodeUpgrade(ctx, sc, sts, partition, services)
 					if err != nil {
-						return status, err
+						return progressingConditions, err
 					}
 					klog.V(2).InfoS("AfterNodeUpgrade hook finished", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(sts))
 				}
@@ -706,13 +753,13 @@ func (scc *Controller) syncStatefulSets(
 				// TODO: Move the pre-node-upgrade hook into a Job.
 				done, err := scc.beforeNodeUpgrade(ctx, sc, sts, nextPartition, services)
 				if err != nil {
-					return status, err
+					return progressingConditions, err
 				}
 
 				if !done {
 					klog.V(4).InfoS("PreNodeUpgrade hook in progress. Waiting a bit.", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(sts))
 					scc.queue.AddAfter(key, 5*time.Second)
-					return status, nil
+					return progressingConditions, nil
 				}
 				klog.V(2).InfoS("PreNodeUpgrade hook finished", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(sts))
 
@@ -744,31 +791,31 @@ func (scc *Controller) syncStatefulSets(
 					return nil
 				})
 				if err != nil {
-					return status, err
+					return progressingConditions, err
 				}
 
 				// Partition can move only one rack a time.
-				return status, nil
+				return progressingConditions, nil
 			}
 
 			status.Upgrade.State = string(PostHooksUpgradePhase)
-			return status, nil
+			return progressingConditions, nil
 
 		case string(PostHooksUpgradePhase):
 			err = scc.afterUpgrade(ctx, sc, services)
 			if err != nil {
-				return status, err
+				return progressingConditions, err
 			}
 
 			status.Upgrade = nil
-			return status, nil
+			return progressingConditions, nil
 
 		default:
 			// An old cluster with an old state machine can still be going through an update, or stuck.
 			// Given have to be reentrant we'll just start again to be sure no step is missed, even a new one.
 			klog.Warning("ScyllaCluster %q has an unknown upgrade phase %q. Resetting the phase.", klog.KObj(sc), status.Upgrade.State)
 			status.Upgrade.State = string(PreHooksUpgradePhase)
-			return status, nil
+			return progressingConditions, nil
 		}
 	}
 
@@ -790,11 +837,11 @@ func (scc *Controller) syncStatefulSets(
 			if requiredVersionLabelPresent && existingVersionLabelPresent {
 				requiredVersion, err := semver.Parse(requiredVersionString)
 				if err != nil {
-					return status, err
+					return progressingConditions, err
 				}
 				existingVersion, err := semver.Parse(existingVersionString)
 				if err != nil {
-					return status, err
+					return progressingConditions, err
 				}
 
 				if requiredVersion.Major != existingVersion.Major ||
@@ -802,6 +849,14 @@ func (scc *Controller) syncStatefulSets(
 					// We need to run hooks for version upgrades.
 					scc.eventRecorder.Eventf(sc, corev1.EventTypeNormal, "UpgradeStarted", "Version changed from %q to %q", existingVersionString, requiredVersionString)
 
+					progressingConditions = append(progressingConditions, metav1.Condition{
+						Type:               statefulSetControllerProgressingCondition,
+						Status:             metav1.ConditionTrue,
+						Reason:             "Upgrading",
+						Message:            "Starting cluster upgrade",
+						ObservedGeneration: sc.Generation,
+					})
+
 					// Initiate the upgrade. This triggers a state machine to run hooks first.
 					now := time.Now()
 					status.Upgrade = &scyllav1.UpgradeStatus{
@@ -811,22 +866,24 @@ func (scc *Controller) syncStatefulSets(
 						SystemSnapshotTag: snapshotTag("system", now),
 						DataSnapshotTag:   snapshotTag("data", now),
 					}
-					return status, nil
+					return progressingConditions, nil
 				}
 			}
 		}
 
 		updatedSts, changed, err := resourceapply.ApplyStatefulSet(ctx, scc.kubeClient.AppsV1(), scc.statefulSetLister, scc.eventRecorder, required, false)
 		if err != nil {
-			return status, fmt.Errorf("can't apply statefulset update: %w", err)
+			return progressingConditions, fmt.Errorf("can't apply statefulset update: %w", err)
 		}
 
 		if changed {
 			anyStsChanged = true
 
+			controllerhelpers.AddGenericProgressingStatusCondition(&progressingConditions, statefulSetControllerProgressingCondition, required, "apply", sc.Generation)
+
 			rackName, ok := updatedSts.Labels[naming.RackNameLabel]
 			if !ok {
-				return status, fmt.Errorf(
+				return progressingConditions, fmt.Errorf(
 					"can't determine rack name: statefulset %s is missing label %q",
 					naming.ObjRef(updatedSts),
 					naming.RackNameLabel,
@@ -839,14 +896,93 @@ func (scc *Controller) syncStatefulSets(
 		// Wait for the StatefulSet to rollout.
 		rolledOut, err := controllerhelpers.IsStatefulSetRolledOut(updatedSts)
 		if err != nil {
-			return status, err
+			return progressingConditions, err
 		}
 
 		if !rolledOut {
 			klog.V(4).InfoS("Waiting for StatefulSet rollout", "ScyllaCluster", klog.KObj(sc), "StatefulSet", klog.KObj(updatedSts))
-			return status, nil
+			progressingConditions = append(progressingConditions, metav1.Condition{
+				Type:               statefulSetControllerProgressingCondition,
+				Status:             metav1.ConditionTrue,
+				Reason:             "WaitingForStatefulSetRollout",
+				Message:            fmt.Sprintf("Waiting for StatefulSet %q to rollout.", naming.ObjRef(required)),
+				ObservedGeneration: sc.Generation,
+			})
+			return progressingConditions, nil
+		}
+	}
+
+	return progressingConditions, nil
+}
+
+func (scc *Controller) setStatefulSetsAvailableStatusCondition(
+	sc *scyllav1.ScyllaCluster,
+	status *scyllav1.ScyllaClusterStatus,
+) {
+	desiredMembers := int32(0)
+	updatedMembers := int32(0)
+	readyMembers := int32(0)
+	var racksInDifferentVersion []string
+	for _, rack := range sc.Spec.Datacenter.Racks {
+		desiredMembers += rack.Members
+
+		rackStatus, found := status.Racks[rack.Name]
+		if !found {
+			klog.Errorf("Can't find status for rack %q", rack.Name)
+			continue
+		}
+
+		if rackStatus.Version != sc.Spec.Version {
+			racksInDifferentVersion = append(racksInDifferentVersion, rack.Name)
 		}
+
+		if rackStatus.Stale == nil || (*rackStatus.Stale) {
+			continue
+		}
+
+		readyMembers += rackStatus.ReadyMembers
+		if rackStatus.UpdatedMembers != nil {
+			updatedMembers += *rackStatus.UpdatedMembers
+		}
+	}
+
+	switch {
+	case len(racksInDifferentVersion) > 0:
+		apimeta.SetStatusCondition(&status.Conditions, metav1.Condition{
+			Type:               statefulSetControllerAvailableCondition,
+			Status:             metav1.ConditionFalse,
+			Reason:             "RacksNotAtDesiredVersion",
+			Message:            fmt.Sprintf("Racks %q are not in the desired version", strings.Join(racksInDifferentVersion, ", ")),
+			ObservedGeneration: sc.Generation,
+		})
+
+	case updatedMembers != desiredMembers:
+		apimeta.SetStatusCondition(&status.Conditions, metav1.Condition{
+			Type:               statefulSetControllerAvailableCondition,
+			Status:             metav1.ConditionFalse,
+			Reason:             "MembersNotUpdated",
+			Message:            fmt.Sprintf("Only %d out of %d member(s) have been updated", updatedMembers, desiredMembers),
+			ObservedGeneration: sc.Generation,
+		})
+
+	case readyMembers != desiredMembers:
+		apimeta.SetStatusCondition(&status.Conditions, metav1.Condition{
+			Type:               statefulSetControllerAvailableCondition,
+			Status:             metav1.ConditionFalse,
+			Reason:             "MembersNotReady",
+			Message:            fmt.Sprintf("Only %d out of %d member(s) are ready", readyMembers, desiredMembers),
+			ObservedGeneration: sc.Generation,
+		})
+
+	default:
+		apimeta.SetStatusCondition(&status.Conditions, metav1.Condition{
+			Type:               statefulSetControllerAvailableCondition,
+			Status:             metav1.ConditionTrue,
+			Reason:             internalapi.AsExpectedReason,
+			Message:            "",
+			ObservedGeneration: sc.Generation,
+		})
 	}
 
-	return status, nil
+	return
 }
diff --git a/pkg/controllerhelpers/core.go b/pkg/controllerhelpers/core.go
index deb229588..a140f89f5 100644
--- a/pkg/controllerhelpers/core.go
+++ b/pkg/controllerhelpers/core.go
@@ -3,10 +3,16 @@ package controllerhelpers
 import (
 	"context"
 	"fmt"
+	"strings"
 
+	"github.com/scylladb/scylla-operator/pkg/internalapi"
 	"github.com/scylladb/scylla-operator/pkg/naming"
+	"github.com/scylladb/scylla-operator/pkg/resource"
+	outilerrors "github.com/scylladb/scylla-operator/pkg/util/errors"
 	corev1 "k8s.io/api/core/v1"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 	corev1schedulinghelpers "k8s.io/component-helpers/scheduling/corev1"
 	"k8s.io/klog/v2"
@@ -31,6 +37,137 @@ func IsPodReady(pod *corev1.Pod) bool {
 	return condition != nil && condition.Status == corev1.ConditionTrue
 }
 
+// FindStatusConditionsWithSuffix finds all conditions that end with the suffix, except the identity.
+func FindStatusConditionsWithSuffix(conditions []metav1.Condition, suffix string) []metav1.Condition {
+	var res []metav1.Condition
+
+	suffixLen := len(suffix)
+	for _, c := range conditions {
+		// Filter out identity and optimize filtering out shorter strings.
+		if len(c.Type) <= suffixLen {
+			continue
+		}
+
+		if strings.HasSuffix(c.Type, suffix) {
+			res = append(res, c)
+		}
+	}
+
+	return res
+}
+
+func aggregateStatusConditionInfo(conditions []metav1.Condition) (string, string) {
+	reasons := make([]string, 0, len(conditions))
+	messages := make([]string, 0, len(conditions))
+
+	for _, c := range conditions {
+		reasons = append(reasons, fmt.Sprintf("%s_%s", c.Type, c.Reason))
+
+		for _, line := range strings.Split(c.Message, "\n") {
+			messages = append(messages, fmt.Sprintf("%s: %s", c.Type, line))
+		}
+	}
+
+	// TODO: Strip aggregated reasons and messages to fit into API limits.
+	return strings.Join(reasons, ","), strings.Join(messages, "\n")
+}
+
+func AggregateStatusConditions(conditions []metav1.Condition, condition metav1.Condition) (metav1.Condition, error) {
+	var defaultVal bool
+	switch condition.Status {
+	case metav1.ConditionTrue:
+		defaultVal = true
+
+	case metav1.ConditionFalse:
+		defaultVal = false
+
+	default:
+		return metav1.Condition{}, fmt.Errorf("unsupported default value %q", condition.Status)
+	}
+
+	var trueConditions, falseConditions, unknownConditions []metav1.Condition
+	for _, c := range conditions {
+		switch c.Status {
+		case metav1.ConditionUnknown:
+			unknownConditions = append(unknownConditions, c)
+
+		case metav1.ConditionTrue:
+			trueConditions = append(trueConditions, c)
+
+		case metav1.ConditionFalse:
+			falseConditions = append(falseConditions, c)
+
+		default:
+			return metav1.Condition{}, fmt.Errorf("unknown condition status %q", c.Status)
+		}
+	}
+
+	if defaultVal == true && len(falseConditions) > 0 {
+		reason, message := aggregateStatusConditionInfo(falseConditions)
+		return metav1.Condition{
+			Type:               condition.Type,
+			Status:             metav1.ConditionFalse,
+			Reason:             reason,
+			Message:            message,
+			ObservedGeneration: condition.ObservedGeneration,
+		}, nil
+	}
+
+	if defaultVal == false && len(trueConditions) > 0 {
+		reason, message := aggregateStatusConditionInfo(trueConditions)
+		return metav1.Condition{
+			Type:               condition.Type,
+			Status:             metav1.ConditionTrue,
+			Reason:             reason,
+			Message:            message,
+			ObservedGeneration: condition.ObservedGeneration,
+		}, nil
+	}
+
+	if len(unknownConditions) > 0 {
+		reason, message := aggregateStatusConditionInfo(unknownConditions)
+		return metav1.Condition{
+			Type:               condition.Type,
+			Status:             metav1.ConditionUnknown,
+			Reason:             reason,
+			Message:            message,
+			ObservedGeneration: condition.ObservedGeneration,
+		}, nil
+	}
+
+	return condition, nil
+}
+
+func SetStatusConditionFromError(conditions *[]metav1.Condition, err error, conditionType string, observedGeneration int64) {
+	if err != nil {
+		apimeta.SetStatusCondition(conditions, metav1.Condition{
+			Type:               conditionType,
+			Status:             metav1.ConditionTrue,
+			Reason:             internalapi.ErrorReason,
+			Message:            outilerrors.NewMultilineAggregate([]error{err}).Error(),
+			ObservedGeneration: observedGeneration,
+		})
+	} else {
+		apimeta.SetStatusCondition(conditions, metav1.Condition{
+			Type:               conditionType,
+			Status:             metav1.ConditionFalse,
+			Reason:             internalapi.AsExpectedReason,
+			Message:            "",
+			ObservedGeneration: observedGeneration,
+		})
+	}
+}
+
+func AddGenericProgressingStatusCondition(conditions *[]metav1.Condition, conditionType string, obj runtime.Object, verb string, observedGeneration int64) {
+	*conditions = append(*conditions, metav1.Condition{
+		Type:               conditionType,
+		Status:             metav1.ConditionTrue,
+		Reason:             internalapi.ProgressingReason,
+		Message:            fmt.Sprintf("Progressing: Running %q on %q", verb, resource.GetObjectGVKOrUnknown(obj)),
+		ObservedGeneration: observedGeneration,
+	})
+}
+
 func IsPodReadyWithPositiveLiveCheck(ctx context.Context, client corev1client.PodsGetter, pod *corev1.Pod) (bool, *corev1.Pod, error) {
 	if !IsPodReady(pod) {
 		return false, pod, nil
diff --git a/pkg/helpers/status.go b/pkg/helpers/status.go
new file mode 100644
index 000000000..ff09a2ac4
--- /dev/null
+++ b/pkg/helpers/status.go
@@ -0,0 +1,29 @@
+package helpers
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func IsStatusConditionPresentAndTrue(conditions []metav1.Condition, conditionType string, generation int64) bool {
+	return IsStatusConditionPresentAndEqual(conditions, conditionType, metav1.ConditionTrue, generation)
+}
+
+func IsStatusConditionPresentAndFalse(conditions []metav1.Condition, conditionType string, generation int64) bool {
+	return IsStatusConditionPresentAndEqual(conditions, conditionType, metav1.ConditionFalse, generation)
+}
+
+func IsStatusConditionPresentAndEqual(conditions []metav1.Condition, conditionType string, status metav1.ConditionStatus, generation int64) bool {
+	for _, condition := range conditions {
+		if condition.Type != conditionType {
+			continue
+		}
+
+		if condition.ObservedGeneration != generation {
+			return false
+		}
+
+		return condition.Status == status
+	}
+
+	return false
+}
diff --git a/pkg/internalapi/conditions.go b/pkg/internalapi/conditions.go
new file mode 100644
index 000000000..cd19abe23
--- /dev/null
+++ b/pkg/internalapi/conditions.go
@@ -0,0 +1,7 @@
+package internalapi
+
+const (
+	AsExpectedReason  = "AsExpected"
+	ErrorReason       = "Error"
+	ProgressingReason = "Progressing"
+)
diff --git a/pkg/resource/helpers.go b/pkg/resource/helpers.go
index ed3ba1ae5..3a9d0893e 100644
--- a/pkg/resource/helpers.go
+++ b/pkg/resource/helpers.go
@@ -2,6 +2,7 @@ package resource
 
 import (
 	"errors"
+	"reflect"
 
 	"github.com/scylladb/scylla-operator/pkg/scheme"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -24,3 +25,25 @@ func GetObjectGVK(object runtime.Object) (*schema.GroupVersionKind, error) {
 
 	return &kinds[0], nil
 }
+
+func GetObjectGVKOrUnknown(obj runtime.Object) *schema.GroupVersionKind {
+	gvk := obj.GetObjectKind().GroupVersionKind()
+	if len(gvk.Kind) > 0 {
+		return &gvk
+	}
+
+	kinds, _, err := scheme.Scheme.ObjectKinds(obj)
+	if err != nil || len(kinds) == 0 {
+		t := reflect.TypeOf(obj)
+		if t.Kind() == reflect.Ptr {
+			t = t.Elem()
+		}
+		return &schema.GroupVersionKind{
+			Group:   "unknown",
+			Version: "unknown",
+			Kind:    t.Name(),
+		}
+	}
+
+	return &kinds[0]
+}
diff --git a/pkg/resourceapply/core.go b/pkg/resourceapply/core.go
index d29aec04d..4ee8e025f 100644
--- a/pkg/resourceapply/core.go
+++ b/pkg/resourceapply/core.go
@@ -52,7 +52,7 @@ func ApplyService(
 		} else {
 			ReportCreateEvent(recorder, requiredCopy, err)
 		}
-		return actual, true, err
+		return actual, err == nil, err
 	}
 
 	existingControllerRef := metav1.GetControllerOfNoCopy(existing)
diff --git a/pkg/util/errors/multiline.go b/pkg/util/errors/multiline.go
new file mode 100644
index 000000000..a70901411
--- /dev/null
+++ b/pkg/util/errors/multiline.go
@@ -0,0 +1,90 @@
+package errors
+
+import (
+	"errors"
+	"strings"
+
+	utilerrors "k8s.io/apimachinery/pkg/util/errors"
+)
+
+type aggregate struct {
+	errList []error
+	sep     string
+}
+
+var _ utilerrors.Aggregate = &aggregate{}
+
+func NewAggregate(errList []error, sep string) error {
+	var errs []error
+
+	for _, err := range errList {
+		if err != nil {
+			errs = append(errs, err)
+		}
+	}
+
+	if len(errs) == 0 {
+		return nil
+	}
+
+	if len(sep) == 0 {
+		sep = "\n"
+	}
+
+	return &aggregate{
+		errList: errs,
+		sep:     sep,
+	}
+}
+
+func NewMultilineAggregate(errList []error) error {
+	return NewAggregate(errList, "\n")
+}
+
+func (agg *aggregate) Error() string {
+	msgs := make([]string, 0, len(agg.errList))
+
+	for _, err := range agg.errList {
+		msgs = append(msgs, err.Error())
+	}
+
+	return strings.Join(msgs, "\n")
+}
+
+func (agg *aggregate) Errors() []error {
+	return agg.errList
+}
+
+func (agg *aggregate) Is(target error) bool {
+	return agg.visit(func(err error) bool {
+		return errors.Is(err, target)
+	})
+}
+
+func (agg *aggregate) visit(f func(err error) bool) bool {
+	for _, err := range agg.errList {
+		switch err := err.(type) {
+		case *aggregate:
+			match := err.visit(f)
+			if match {
+				return match
+			}
+
+		case utilerrors.Aggregate:
+			for _, nestedErr := range err.Errors() {
+				match := f(nestedErr)
+				if match {
+					return match
+				}
+			}
+
+		default:
+			match := f(err)
+			if match {
+				return match
+			}
+		}
+	}
+
+	return false
+}


Test patch:
diff --git a/pkg/controllerhelpers/core_test.go b/pkg/controllerhelpers/core_test.go
new file mode 100644
index 000000000..cd67040ee
--- /dev/null
+++ b/pkg/controllerhelpers/core_test.go
@@ -0,0 +1,360 @@
+package controllerhelpers
+
+import (
+	"errors"
+	"reflect"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	apiequality "k8s.io/apimachinery/pkg/api/equality"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestFindStatusConditionsWithSuffix(t *testing.T) {
+	tt := []struct {
+		name       string
+		conditions []metav1.Condition
+		suffix     string
+		expected   []metav1.Condition
+	}{
+		{
+			name:       "won't panic with nil array",
+			conditions: nil,
+			suffix:     "",
+			expected:   nil,
+		},
+		{
+			name:   "finds matching elements",
+			suffix: "Progressing",
+			conditions: []metav1.Condition{
+				{
+					Type:    "FooProgressing",
+					Status:  metav1.ConditionTrue,
+					Reason:  "FooProgressingReason",
+					Message: "FooProgressing message",
+				},
+				{
+					Type:    "Progressing",
+					Status:  metav1.ConditionTrue,
+					Reason:  "ProgressingReason",
+					Message: "Progressing message",
+				},
+				{
+					Type:    "BarProgressing",
+					Status:  metav1.ConditionFalse,
+					Reason:  "BarProgressingReason",
+					Message: "BarProgressing message",
+				},
+			},
+			expected: []metav1.Condition{
+				{
+					Type:    "FooProgressing",
+					Status:  metav1.ConditionTrue,
+					Reason:  "FooProgressingReason",
+					Message: "FooProgressing message",
+				},
+				{
+					Type:    "BarProgressing",
+					Status:  metav1.ConditionFalse,
+					Reason:  "BarProgressingReason",
+					Message: "BarProgressing message",
+				},
+			},
+		},
+	}
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			got := FindStatusConditionsWithSuffix(tc.conditions, tc.suffix)
+			if !apiequality.Semantic.DeepEqual(got, tc.expected) {
+				t.Errorf("expected and got differ: %s", cmp.Diff(tc.expected, got))
+			}
+		})
+	}
+}
+
+func TestAggregateStatusConditions(t *testing.T) {
+	tt := []struct {
+		name              string
+		conditions        []metav1.Condition
+		condition         metav1.Condition
+		expectedCondition metav1.Condition
+		expectedError     error
+	}{
+		{
+			name:       "won't panic with nil array",
+			conditions: nil,
+			condition: metav1.Condition{
+				Status:             metav1.ConditionTrue,
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{
+				Status:             metav1.ConditionTrue,
+				ObservedGeneration: 42,
+			},
+			expectedError: nil,
+		},
+		{
+			name:       "will error on unknown initial condition",
+			conditions: nil,
+			condition: metav1.Condition{
+				Status:             metav1.ConditionUnknown,
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{},
+			expectedError:     errors.New(`unsupported default value "Unknown"`),
+		},
+		{
+			name: "positive condition with all true results in the initial condition",
+			conditions: []metav1.Condition{
+				{
+					Type:    "FooAvailable",
+					Status:  metav1.ConditionTrue,
+					Reason:  "FooAvailableReason",
+					Message: "FooAvailable ok",
+				},
+				{
+					Type:    "BarAvailable",
+					Status:  metav1.ConditionTrue,
+					Reason:  "BarAvailableReason",
+					Message: "BarAvailable ok",
+				},
+			},
+			condition: metav1.Condition{
+				Type:               "Available",
+				Status:             metav1.ConditionTrue,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{
+				Type:               "Available",
+				Status:             metav1.ConditionTrue,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedError: nil,
+		},
+		{
+			name: "positive condition with true and unknown results in an unknown condition",
+			conditions: []metav1.Condition{
+				{
+					Type:    "FooAvailable",
+					Status:  metav1.ConditionTrue,
+					Reason:  "FooAvailableReason",
+					Message: "FooAvailable ok",
+				},
+				{
+					Type:    "BarAvailable",
+					Status:  metav1.ConditionTrue,
+					Reason:  "BarAvailableReason",
+					Message: "BarAvailable ok",
+				},
+				{
+					Type:    "UnknownAvailableFoo",
+					Status:  metav1.ConditionUnknown,
+					Reason:  "UnknownAvailableFooReason",
+					Message: "UnknownAvailableFoo message",
+				},
+				{
+					Type:    "UnknownAvailableBar",
+					Status:  metav1.ConditionUnknown,
+					Reason:  "UnknownAvailableBarReason",
+					Message: "UnknownAvailableBar message",
+				},
+			},
+			condition: metav1.Condition{
+				Type:               "Available",
+				Status:             metav1.ConditionTrue,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{
+				Type:               "Available",
+				Status:             metav1.ConditionUnknown,
+				Reason:             "UnknownAvailableFoo_UnknownAvailableFooReason,UnknownAvailableBar_UnknownAvailableBarReason",
+				Message:            "UnknownAvailableFoo: UnknownAvailableFoo message\nUnknownAvailableBar: UnknownAvailableBar message",
+				ObservedGeneration: 42,
+			},
+			expectedError: nil,
+		},
+		{
+			name: "positive condition with some false aggregates the false states",
+			conditions: []metav1.Condition{
+				{
+					Type:    "FooAvailable",
+					Status:  metav1.ConditionFalse,
+					Reason:  "FooAvailableReason",
+					Message: "FooAvailable error",
+				},
+				{
+					Type:    "GoodAvailable",
+					Status:  metav1.ConditionTrue,
+					Reason:  "GoodAvailableReason",
+					Message: "GoodAvailable ok",
+				},
+				{
+					Type:    "UnknownAvailable",
+					Status:  metav1.ConditionTrue,
+					Reason:  "UnknownAvailableReason",
+					Message: "UnknownAvailable ok",
+				},
+				{
+					Type:    "BarAvailable",
+					Status:  metav1.ConditionFalse,
+					Reason:  "BarAvailableReason",
+					Message: "BarAvailable error",
+				},
+			},
+			condition: metav1.Condition{
+				Type:               "Available",
+				Status:             metav1.ConditionTrue,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{
+				Type:               "Available",
+				Status:             metav1.ConditionFalse,
+				Reason:             "FooAvailable_FooAvailableReason,BarAvailable_BarAvailableReason",
+				Message:            "FooAvailable: FooAvailable error\nBarAvailable: BarAvailable error",
+				ObservedGeneration: 42,
+			},
+			expectedError: nil,
+		},
+		{
+			name: "negative condition with all false results in the initial condition",
+			conditions: []metav1.Condition{
+				{
+					Type:    "FooDegraded",
+					Status:  metav1.ConditionFalse,
+					Reason:  "FooDegradedReason",
+					Message: "FooDegraded ok",
+				},
+				{
+					Type:    "BarDegraded",
+					Status:  metav1.ConditionFalse,
+					Reason:  "BarDegradedReason",
+					Message: "BarDegraded ok",
+				},
+			},
+			condition: metav1.Condition{
+				Type:               "Degraded",
+				Status:             metav1.ConditionFalse,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{
+				Type:               "Degraded",
+				Status:             metav1.ConditionFalse,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedError: nil,
+		},
+		{
+			name: "negative condition with false and unknown results in an unknown condition",
+			conditions: []metav1.Condition{
+				{
+					Type:    "FooDegraded",
+					Status:  metav1.ConditionFalse,
+					Reason:  "FooDegradedReason",
+					Message: "FooDegraded ok",
+				},
+				{
+					Type:    "BarDegraded",
+					Status:  metav1.ConditionFalse,
+					Reason:  "BarDegradedReason",
+					Message: "BarDegraded ok",
+				},
+				{
+					Type:    "UnknownDegradedFoo",
+					Status:  metav1.ConditionUnknown,
+					Reason:  "UnknownDegradedFooReason",
+					Message: "UnknownDegradedFoo message",
+				},
+				{
+					Type:    "UnknownDegradedBar",
+					Status:  metav1.ConditionUnknown,
+					Reason:  "UnknownDegradedBar reason",
+					Message: "UnknownDegradedBar message",
+				},
+			},
+			condition: metav1.Condition{
+				Type:               "Degraded",
+				Status:             metav1.ConditionFalse,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{
+				Type:               "Degraded",
+				Status:             metav1.ConditionUnknown,
+				Reason:             "UnknownDegradedFoo_UnknownDegradedFooReason,UnknownDegradedBar_UnknownDegradedBar reason",
+				Message:            "UnknownDegradedFoo: UnknownDegradedFoo message\nUnknownDegradedBar: UnknownDegradedBar message",
+				ObservedGeneration: 42,
+			},
+			expectedError: nil,
+		},
+		{
+			name: "negative condition with some true aggregates the true states",
+			conditions: []metav1.Condition{
+				{
+					Type:    "FooDegraded",
+					Status:  metav1.ConditionTrue,
+					Reason:  "FooDegradedReason",
+					Message: "FooDegraded error",
+				},
+				{
+					Type:    "GoodDegraded",
+					Status:  metav1.ConditionFalse,
+					Reason:  "GoodDegradedReason",
+					Message: "GoodDegraded ok",
+				},
+				{
+					Type:    "UnknownDegraded",
+					Status:  metav1.ConditionFalse,
+					Reason:  "UnknownDegradedReason",
+					Message: "UnknownDegraded ok",
+				},
+				{
+					Type:    "BarDegraded",
+					Status:  metav1.ConditionTrue,
+					Reason:  "BarDegradedReason",
+					Message: "BarDegraded error",
+				},
+			},
+			condition: metav1.Condition{
+				Type:               "Degraded",
+				Status:             metav1.ConditionFalse,
+				Reason:             "AsExpected",
+				Message:            "AsExpected",
+				ObservedGeneration: 42,
+			},
+			expectedCondition: metav1.Condition{
+				Type:               "Degraded",
+				Status:             metav1.ConditionTrue,
+				Reason:             "FooDegraded_FooDegradedReason,BarDegraded_BarDegradedReason",
+				Message:            "FooDegraded: FooDegraded error\nBarDegraded: BarDegraded error",
+				ObservedGeneration: 42,
+			},
+			expectedError: nil,
+		},
+	}
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			got, err := AggregateStatusConditions(tc.conditions, tc.condition)
+
+			if !reflect.DeepEqual(err, tc.expectedError) {
+				t.Errorf("expected error %#v, got %#v", tc.expectedError, err)
+			}
+
+			if !apiequality.Semantic.DeepEqual(got, tc.expectedCondition) {
+				t.Errorf("expected and got differ: %s", cmp.Diff(tc.expectedCondition, got))
+			}
+		})
+	}
+}
diff --git a/pkg/util/errors/multiline_test.go b/pkg/util/errors/multiline_test.go
new file mode 100644
index 000000000..6944c4671
--- /dev/null
+++ b/pkg/util/errors/multiline_test.go
@@ -0,0 +1,45 @@
+package errors
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestNewAggregate(t *testing.T) {
+	tt := []struct {
+		name           string
+		errList        []error
+		sep            string
+		expectedString string
+	}{
+		{
+			name:           "nil list results in an empty string",
+			errList:        nil,
+			sep:            "\n",
+			expectedString: "",
+		},
+		{
+			name: "multiline error is correctly formatted",
+			errList: []error{
+				errors.New("foo"),
+				nil,
+				errors.New("bar"),
+			},
+			sep:            "\n",
+			expectedString: "foo\nbar",
+		},
+	}
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			got := NewAggregate(tc.errList, tc.sep)
+
+			var gotString string
+			if got != nil {
+				gotString = got.Error()
+			}
+			if gotString != tc.expectedString {
+				t.Errorf("expected error %q, got %q", tc.expectedString, gotString)
+			}
+		})
+	}
+}
diff --git a/test/e2e/set/scyllacluster/verify.go b/test/e2e/set/scyllacluster/verify.go
index 39c4ad6b2..94b95f406 100644
--- a/test/e2e/set/scyllacluster/verify.go
+++ b/test/e2e/set/scyllacluster/verify.go
@@ -83,9 +83,118 @@ func verifyPodDisruptionBudget(sc *scyllav1.ScyllaCluster, pdb *policyv1.PodDisr
 func verifyScyllaCluster(ctx context.Context, kubeClient kubernetes.Interface, sc *scyllav1.ScyllaCluster) {
 	framework.By("Verifying the ScyllaCluster")
 
+	sc = sc.DeepCopy()
+
+	o.Expect(sc.CreationTimestamp).NotTo(o.BeNil())
 	o.Expect(sc.Status.ObservedGeneration).NotTo(o.BeNil())
+	o.Expect(*sc.Status.ObservedGeneration).To(o.BeNumerically(">=", sc.Generation))
 	o.Expect(sc.Status.Racks).To(o.HaveLen(len(sc.Spec.Datacenter.Racks)))
 
+	for i := range sc.Status.Conditions {
+		c := &sc.Status.Conditions[i]
+		o.Expect(c.LastTransitionTime).NotTo(o.BeNil())
+		o.Expect(c.LastTransitionTime.Time.Before(sc.CreationTimestamp.Time)).NotTo(o.BeTrue())
+
+		// To be able to compare the statuses we need to remove the random timestamp.
+		c.LastTransitionTime = metav1.Time{}
+	}
+	o.Expect(sc.Status.Conditions).To(o.ConsistOf(func() []interface{} {
+		type condValue struct {
+			condType string
+			status   metav1.ConditionStatus
+		}
+		condList := []condValue{
+			// Aggregated conditions
+			{
+				condType: "Available",
+				status:   metav1.ConditionTrue,
+			},
+			{
+				condType: "Progressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "Degraded",
+				status:   metav1.ConditionFalse,
+			},
+
+			// Controller conditions
+			{
+				condType: "ServiceAccountControllerProgressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "ServiceAccountControllerDegraded",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "RoleBindingControllerProgressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "RoleBindingControllerDegraded",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "AgentTokenControllerProgressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "AgentTokenControllerDegraded",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "StatefulSetControllerAvailable",
+				status:   metav1.ConditionTrue,
+			},
+			{
+				condType: "StatefulSetControllerProgressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "StatefulSetControllerDegraded",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "ServiceControllerProgressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "ServiceControllerDegraded",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "PDBControllerProgressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "PDBControllerDegraded",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "IngressControllerProgressing",
+				status:   metav1.ConditionFalse,
+			},
+			{
+				condType: "IngressControllerDegraded",
+				status:   metav1.ConditionFalse,
+			},
+		}
+
+		expectedConditions := make([]interface{}, 0, len(condList))
+		for _, item := range condList {
+			expectedConditions = append(expectedConditions, metav1.Condition{
+				Type:               item.condType,
+				Status:             item.status,
+				Reason:             "AsExpected",
+				Message:            "",
+				ObservedGeneration: sc.Generation,
+			})
+		}
+
+		return expectedConditions
+	}()...))
+
 	statefulsets, err := utils.GetStatefulSetsForScyllaCluster(ctx, kubeClient.AppsV1(), sc)
 	o.Expect(err).NotTo(o.HaveOccurred())
 	o.Expect(statefulsets).To(o.HaveLen(len(sc.Spec.Datacenter.Racks)))
diff --git a/test/e2e/utils/helpers.go b/test/e2e/utils/helpers.go
index 3f978ff50..87a468ec1 100644
--- a/test/e2e/utils/helpers.go
+++ b/test/e2e/utils/helpers.go
@@ -108,50 +108,15 @@ func ContextForManagerSync(parent context.Context, sc *scyllav1.ScyllaCluster) (
 }
 
 func IsScyllaClusterRolledOut(sc *scyllav1.ScyllaCluster) (bool, error) {
-	// ObservedGeneration == nil will filter out the case when the object is initially created
-	// so no other optional (but required) field should be nil after this point and we should error out.
-	if sc.Status.ObservedGeneration == nil || *sc.Status.ObservedGeneration < sc.Generation {
+	if !helpers.IsStatusConditionPresentAndTrue(sc.Status.Conditions, scyllav1.AvailableCondition, sc.Generation) {
 		return false, nil
 	}
 
-	// TODO: this should be more straight forward - we need better status (conditions, aggregated state, ...)
-
-	for _, r := range sc.Spec.Datacenter.Racks {
-		rackStatus, found := sc.Status.Racks[r.Name]
-		if !found {
-			return false, nil
-		}
-
-		if rackStatus.Stale == nil {
-			return true, fmt.Errorf("stale shouldn't be nil")
-		}
-
-		if *rackStatus.Stale {
-			return false, nil
-		}
-
-		if rackStatus.Members != r.Members {
-			return false, nil
-		}
-
-		if rackStatus.ReadyMembers != r.Members {
-			return false, nil
-		}
-
-		if rackStatus.UpdatedMembers == nil {
-			return true, fmt.Errorf("updatedMembers shouldn't be nil")
-		}
-
-		if *rackStatus.UpdatedMembers != r.Members {
-			return false, nil
-		}
-
-		if rackStatus.Version != sc.Spec.Version {
-			return false, nil
-		}
+	if !helpers.IsStatusConditionPresentAndFalse(sc.Status.Conditions, scyllav1.ProgressingCondition, sc.Generation) {
+		return false, nil
 	}
 
-	if sc.Status.Upgrade != nil && sc.Status.Upgrade.FromVersion != sc.Status.Upgrade.ToVersion {
+	if !helpers.IsStatusConditionPresentAndFalse(sc.Status.Conditions, scyllav1.DegradedCondition, sc.Generation) {
 		return false, nil
 	}