Repository: FoundationDB/fdb-kubernetes-operator
Pull request #1641 (2023-05-22 16:29:41)

Update e2e README.md
## Description

running the provided commands threw an error:

```
fdb-kubernetes-operator/e2e/scripts/setup_e2e.sh: line 74: helm: command not found
```

Installing helm in the testing machine fixed it. 

In this PR we update the docs to include this dependency.

# Description

*Please include a summary of the change and which issue is addressed. If this change resolves an issue, please include the issue number in the description.*

## Type of change

*Please select one of the options below.*

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Documentation
- Other

## Discussion

*Are there any design details that you would like to discuss further?*

## Testing

*Please describe the tests that you ran to verify your changes. Unit tests?
Manual testing?*

*Do we need to perform additional testing once this is merged, or perform in a larger testing environment?*

## Documentation

*Did you update relevant documentation within this repository?*

*If this change is adding new functionality, do we need to describe it in our user manual?*

*If this change is adding or removing subreconcilers, have we updated the core technical design doc to reflect that?*

*If this change is adding new safety checks or new potential failure modes, have we documented and how to debug potential issues?*

## Follow-up

*Are there any follow-up issues that we should pursue in the future?*

*Does this introduce new defaults that we should re-evaluate in the future?*


foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr on Linux CentOS 7

* Commit ID: 4a1d3ea73372aacaa4957fdc35f29af40414d395
* Duration 3:07:26
* Result: :white_check_mark: **SUCCEEDED**
* Error: `N/A`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr/c7c041fb-5e82-4feb-8a31-a04ebb25ef76.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr/c7c041fb-5e82-4feb-8a31-a04ebb25ef76/workspace.zip) zip file of the working directory (available for 30 days)

foundationdb-ci: 
### Result of fdb-kubernetes-operator-pr-kind on Linux CentOS 7

* Commit ID: 4a1d3ea73372aacaa4957fdc35f29af40414d395
* Duration 4:12:46
* Result: :x: **FAILED**
* Error: `Error while executing command: if $fail_test; then exit 1; fi. Reason: exit status 1`
* [Build Log](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator/fdb-kubernetes-operator-pr-kind/d6380e23-0731-447a-a9f6-d48ffb69680e.gz) terminal output (available for 30 days)
* [Build Workspace](https://d1e0l78xbkh3xa.cloudfront.net/fdb-kubernetes-operator-pr-kind/d6380e23-0731-447a-a9f6-d48ffb69680e/workspace.zip) zip file of the working directory (available for 30 days)


Patch:
diff --git a/api/v1beta2/foundationdbcluster_types.go b/api/v1beta2/foundationdbcluster_types.go
index ddb2e11e..08464350 100644
--- a/api/v1beta2/foundationdbcluster_types.go
+++ b/api/v1beta2/foundationdbcluster_types.go
@@ -346,9 +346,27 @@ func (processGroupStatus *ProcessGroupStatus) IsExcluded() bool {
 	return (processGroupStatus.ExclusionTimestamp != nil && !processGroupStatus.ExclusionTimestamp.IsZero()) || processGroupStatus.ExclusionSkipped
 }
 
-// SetExclude marks a process group as excluded
+// SetExclude marks a process group as excluded and will reset the process group conditions to only include the ResourcesTerminating
+// if already set, otherwise the conditions will be an empty slice. This reflects the operator behaviour that process
+// groups that are marked for removal and are fully excluded will only have the ResourcesTerminating condition.
 func (processGroupStatus *ProcessGroupStatus) SetExclude() {
+	if !processGroupStatus.ExclusionTimestamp.IsZero() {
+		return
+	}
+
 	processGroupStatus.ExclusionTimestamp = &metav1.Time{Time: time.Now()}
+	// Reset all previous conditions as the operator will only track the ResourcesTerminating condition for process
+	// groups marked as removal. If the ResourcesTerminating condition is already set we are not removing it.
+	newConditions := make([]*ProcessGroupCondition, 0, 1)
+	for _, condition := range processGroupStatus.ProcessGroupConditions {
+		if condition.ProcessGroupConditionType != ResourcesTerminating {
+			continue
+		}
+
+		newConditions = append(newConditions, condition)
+	}
+
+	processGroupStatus.ProcessGroupConditions = newConditions
 }
 
 // IsMarkedForRemoval returns if a process group is marked for removal
@@ -356,8 +374,12 @@ func (processGroupStatus *ProcessGroupStatus) IsMarkedForRemoval() bool {
 	return processGroupStatus.RemovalTimestamp != nil && !processGroupStatus.RemovalTimestamp.IsZero()
 }
 
-// MarkForRemoval marks a process group for removal
+// MarkForRemoval marks a process group for removal. If the RemovalTimestamp is already set it won't be changed.
 func (processGroupStatus *ProcessGroupStatus) MarkForRemoval() {
+	if !processGroupStatus.RemovalTimestamp.IsZero() {
+		return
+	}
+
 	processGroupStatus.RemovalTimestamp = &metav1.Time{Time: time.Now()}
 }
 
@@ -548,14 +570,13 @@ func MarkProcessGroupForRemoval(processGroups []*ProcessGroupStatus, processGrou
 }
 
 // UpdateCondition will add or remove a condition in the ProcessGroupStatus.
-// If the old ProcessGroupStatus already contains the condition, and the condition is being set,
-// the condition is reused to contain the same timestamp.
-func (processGroupStatus *ProcessGroupStatus) UpdateCondition(conditionType ProcessGroupConditionType, set bool, oldProcessGroups []*ProcessGroupStatus, processGroupID ProcessGroupID) {
+func (processGroupStatus *ProcessGroupStatus) UpdateCondition(conditionType ProcessGroupConditionType, set bool) {
 	if set {
-		processGroupStatus.addCondition(oldProcessGroups, processGroupID, conditionType)
-	} else {
-		processGroupStatus.removeCondition(conditionType)
+		processGroupStatus.addCondition(conditionType)
+		return
 	}
+
+	processGroupStatus.removeCondition(conditionType)
 }
 
 // UpdateConditionTime will update the conditionType's condition time to newTime
@@ -569,38 +590,21 @@ func (processGroupStatus *ProcessGroupStatus) UpdateConditionTime(conditionType
 	}
 }
 
-// addCondition will add the condition to the ProcessGroupStatus.
-// If the old ProcessGroupStatus already contains the condition the condition is reused to contain the same timestamp.
-func (processGroupStatus *ProcessGroupStatus) addCondition(oldProcessGroups []*ProcessGroupStatus, processGroupID ProcessGroupID, conditionType ProcessGroupConditionType) {
-	var oldProcessGroupStatus *ProcessGroupStatus
-
-	// Check if we got a ProcessGroupStatus for the processGroupID
-	for _, oldGroupStatus := range oldProcessGroups {
-		if oldGroupStatus.ProcessGroupID != processGroupID {
-			continue
-		}
-
-		oldProcessGroupStatus = oldGroupStatus
-		break
-	}
-
-	// Check if we already got this condition in the current ProcessGroupStatus
-	// This check must execute before checking oldProcessGroupStatus; otherwise, we will add duplicate condition
-	// when both processGroupStatus and oldProcessGroupStatus have the conditionType
+// addCondition will add the condition to the ProcessGroupStatus. If the condition is already present this method will not
+// change the timestamp. If a process group is marked for removal and exclusion only the ResourcesTerminating can be added
+// and all other conditions will be reset.
+func (processGroupStatus *ProcessGroupStatus) addCondition(conditionType ProcessGroupConditionType) {
+	// Check if we already got this condition in the current ProcessGroupStatus.
 	for _, condition := range processGroupStatus.ProcessGroupConditions {
 		if condition.ProcessGroupConditionType == conditionType {
 			return
 		}
 	}
 
-	// Check if we got a condition for the condition type for the ProcessGroupStatus
-	if oldProcessGroupStatus != nil {
-		for _, condition := range oldProcessGroupStatus.ProcessGroupConditions {
-			if condition.ProcessGroupConditionType == conditionType {
-				// Assumption: processGroupStatus doesn't have the conditionType
-				processGroupStatus.ProcessGroupConditions = append(processGroupStatus.ProcessGroupConditions, condition)
-				return
-			}
+	// If a process group is marked for removal and is fully excluded we only keep the ResourcesTerminating condition.
+	if processGroupStatus.IsMarkedForRemoval() && processGroupStatus.IsExcluded() {
+		if conditionType != ResourcesTerminating {
+			return
 		}
 	}
 
diff --git a/controllers/bounce_processes.go b/controllers/bounce_processes.go
index c7d4fb59..eadcc9f1 100644
--- a/controllers/bounce_processes.go
+++ b/controllers/bounce_processes.go
@@ -167,6 +167,7 @@ func (bounceProcesses) reconcile(ctx context.Context, r *FoundationDBClusterReco
 func getProcessesReadyForRestart(logger logr.Logger, cluster *fdbv1beta2.FoundationDBCluster, addressMap map[fdbv1beta2.ProcessGroupID][]fdbv1beta2.ProcessAddress) ([]fdbv1beta2.ProcessAddress, *requeue) {
 	addresses := make([]fdbv1beta2.ProcessAddress, 0, len(cluster.Status.ProcessGroups))
 	allSynced := true
+	versionIncompatibleUpgrade := cluster.IsBeingUpgradedWithVersionIncompatibleVersion()
 	var missingAddress []fdbv1beta2.ProcessGroupID
 
 	filterConditions := restarts.GetFilterConditions(cluster)
@@ -186,6 +187,13 @@ func getProcessesReadyForRestart(logger logr.Logger, cluster *fdbv1beta2.Foundat
 		// of cluster.GetProcessCountsWithDefaults() + the number of process groups marked for removal.
 		if processGroup.IsMarkedForRemoval() {
 			markedForRemoval++
+			// If we do a version incompatible upgrade we want to add the excluded processes to the list of processes
+			// that should be restarted, to make sure we restart all processes in the cluster.
+			if versionIncompatibleUpgrade && processGroup.IsExcluded() {
+				logger.Info("adding process group that is marked for exclusion to list of restarted processes", "processGroupID", processGroup.ProcessGroupID)
+				addresses = append(addresses, addressMap[processGroup.ProcessGroupID]...)
+				continue
+			}
 		}
 
 		// Ignore processes that are missing for more than 30 seconds e.mg. if the process is network partitioned.
diff --git a/controllers/update_pod_config.go b/controllers/update_pod_config.go
index f527eeda..465e0107 100644
--- a/controllers/update_pod_config.go
+++ b/controllers/update_pod_config.go
@@ -107,9 +107,9 @@ func (updatePodConfig) reconcile(ctx context.Context, r *FoundationDBClusterReco
 
 			if internal.IsNetworkError(err) && processGroup.GetConditionTime(fdbv1beta2.SidecarUnreachable) == nil {
 				curLogger.Info("process group sidecar is not reachable")
-				processGroup.UpdateCondition(fdbv1beta2.SidecarUnreachable, true, cluster.Status.ProcessGroups, processGroup.ProcessGroupID)
+				processGroup.UpdateCondition(fdbv1beta2.SidecarUnreachable, true)
 			} else if processGroup.GetConditionTime(fdbv1beta2.IncorrectConfigMap) == nil {
-				processGroup.UpdateCondition(fdbv1beta2.IncorrectConfigMap, true, cluster.Status.ProcessGroups, processGroup.ProcessGroupID)
+				processGroup.UpdateCondition(fdbv1beta2.IncorrectConfigMap, true)
 				// If we are still waiting for a ConfigMap update we should not delay the requeue to ensure all processes are bounced
 				// at the same time. If the process is unreachable e.g. has the SidecarUnreachable status we can delay the requeue.
 				delayedRequeue = false
@@ -137,7 +137,7 @@ func (updatePodConfig) reconcile(ctx context.Context, r *FoundationDBClusterReco
 			}
 		}
 
-		processGroup.UpdateCondition(fdbv1beta2.SidecarUnreachable, false, cluster.Status.ProcessGroups, processGroup.ProcessGroupID)
+		processGroup.UpdateCondition(fdbv1beta2.SidecarUnreachable, false)
 	}
 
 	if !equality.Semantic.DeepEqual(cluster.Status, *originalStatus) {
diff --git a/controllers/update_status.go b/controllers/update_status.go
index 826450db..0d5da61f 100644
--- a/controllers/update_status.go
+++ b/controllers/update_status.go
@@ -175,7 +175,6 @@ func (updateStatus) reconcile(ctx context.Context, r *FoundationDBClusterReconci
 	if err != nil {
 		return &requeue{curError: fmt.Errorf("update_status skipped due to error in validateProcessGroups: %w", err)}
 	}
-	removeDuplicateConditions(status)
 
 	existingConfigMap := &corev1.ConfigMap{}
 	err = r.Get(ctx, types.NamespacedName{Namespace: configMap.Namespace, Name: configMap.Name}, existingConfigMap)
@@ -358,7 +357,7 @@ func checkAndSetProcessStatus(r *FoundationDBClusterReconciler, cluster *fdbv1be
 
 	processStatus := processMap[processID]
 
-	processGroupStatus.UpdateCondition(fdbv1beta2.MissingProcesses, len(processStatus) == 0, cluster.Status.ProcessGroups, processID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.MissingProcesses, len(processStatus) == 0)
 	if len(processStatus) == 0 {
 		return nil
 	}
@@ -375,7 +374,7 @@ func checkAndSetProcessStatus(r *FoundationDBClusterReconciler, cluster *fdbv1be
 		commandLine, err := internal.GetStartCommand(cluster, processGroupStatus.ProcessClass, podClient, processNumber, processCount)
 		if err != nil {
 			if internal.IsNetworkError(err) {
-				processGroupStatus.UpdateCondition(fdbv1beta2.SidecarUnreachable, true, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+				processGroupStatus.UpdateCondition(fdbv1beta2.SidecarUnreachable, true)
 				return nil
 			}
 
@@ -400,9 +399,9 @@ func checkAndSetProcessStatus(r *FoundationDBClusterReconciler, cluster *fdbv1be
 		}
 	}
 
-	processGroupStatus.UpdateCondition(fdbv1beta2.IncorrectCommandLine, !correct, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.IncorrectCommandLine, !correct)
 	// Reset status for sidecar unreachable, since we are here at this point we were able to reach the sidecar for the substitute variables.
-	processGroupStatus.UpdateCondition(fdbv1beta2.SidecarUnreachable, false, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.SidecarUnreachable, false)
 
 	return nil
 }
@@ -426,21 +425,31 @@ func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler
 	}
 
 	for _, processGroup := range processGroups {
+		// If the process group should be removed mark it for removal.
+		if cluster.ProcessGroupIsBeingRemoved(processGroup.ProcessGroupID) {
+			processGroup.MarkForRemoval()
+			// Check if we should skip exclusion for the process group
+			_, ok := processGroupsWithoutExclusion[processGroup.ProcessGroupID]
+			if ok {
+				processGroup.ExclusionSkipped = ok
+				processGroup.SetExclude()
+			}
+		}
+
 		pod, podError := r.PodLifecycleManager.GetPod(ctx, r, cluster, processGroup.GetPodName(cluster))
 
 		// If the process group is not being removed and the Pod is not set we need to put it into
 		// the failing list.
-		isBeingRemoved := cluster.ProcessGroupIsBeingRemoved(processGroup.ProcessGroupID)
 		if k8serrors.IsNotFound(podError) {
 			// Mark process groups as terminating if the pod has been deleted but other
 			// resources are stuck in terminating.
-			if isBeingRemoved {
-				processGroup.UpdateCondition(fdbv1beta2.ResourcesTerminating, true, processGroups, processGroup.ProcessGroupID)
+			if processGroup.IsMarkedForRemoval() && processGroup.IsExcluded() {
+				processGroup.UpdateCondition(fdbv1beta2.ResourcesTerminating, true)
 			} else {
-				processGroup.UpdateCondition(fdbv1beta2.MissingPod, true, processGroups, processGroup.ProcessGroupID)
+				processGroup.UpdateCondition(fdbv1beta2.MissingPod, true)
 			}
 
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, false, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, false)
 			continue
 		}
 
@@ -449,9 +458,9 @@ func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler
 
 		// In this case the Pod has a DeletionTimestamp and should be deleted.
 		if !pod.ObjectMeta.DeletionTimestamp.IsZero() {
-			// If the ProcessGroup is marked for removal we can put the status into ResourcesTerminating
-			if processGroup.IsMarkedForRemoval() {
-				processGroup.UpdateCondition(fdbv1beta2.ResourcesTerminating, true, processGroups, processGroup.ProcessGroupID)
+			// If the ProcessGroup is marked for removal and is excluded, we can put the status into ResourcesTerminating.
+			if processGroup.IsMarkedForRemoval() && processGroup.IsExcluded() {
+				processGroup.UpdateCondition(fdbv1beta2.ResourcesTerminating, true)
 				continue
 			}
 			// Otherwise we set PodFailing to ensure that the operator will trigger a replacement. This case can happen
@@ -459,11 +468,11 @@ func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler
 			// cluster. We only set this condition if the Pod is in this state for GetFailedPodDuration(), the default
 			// here is 5 minutes.
 			if pod.ObjectMeta.DeletionTimestamp.Add(cluster.GetFailedPodDuration()).Before(time.Now()) {
-				processGroup.UpdateCondition(fdbv1beta2.PodFailing, true, processGroups, processGroup.ProcessGroupID)
+				processGroup.UpdateCondition(fdbv1beta2.PodFailing, true)
 				continue
 			}
 
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, false, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, false)
 		}
 
 		// Even the process group will be removed we need to keep the config around.
@@ -490,15 +499,6 @@ func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler
 			status.ImageTypes = append(status.ImageTypes, imageTypeString)
 		}
 
-		if isBeingRemoved {
-			if processGroup.RemovalTimestamp.IsZero() {
-				processGroup.MarkForRemoval()
-			}
-			// Check if we should skip exclusion for the process group
-			_, ok := processGroupsWithoutExclusion[processGroup.ProcessGroupID]
-			processGroup.ExclusionSkipped = ok
-		}
-
 		if pod.ObjectMeta.DeletionTimestamp.IsZero() && status.HasListenIPsForAllPods {
 			hasPodIP := false
 			for _, container := range pod.Spec.Containers {
@@ -548,7 +548,7 @@ func validateProcessGroups(ctx context.Context, r *FoundationDBClusterReconciler
 func validateProcessGroup(ctx context.Context, r *FoundationDBClusterReconciler, cluster *fdbv1beta2.FoundationDBCluster,
 	pod *corev1.Pod, currentPVC *corev1.PersistentVolumeClaim, configMapHash string, processGroupStatus *fdbv1beta2.ProcessGroupStatus,
 	disableTaintFeature bool, nodeMap map[string]*corev1.Node, logger logr.Logger) error {
-	processGroupStatus.UpdateCondition(fdbv1beta2.MissingPod, pod == nil, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.MissingPod, pod == nil)
 	if pod == nil {
 		return nil
 	}
@@ -572,7 +572,7 @@ func validateProcessGroup(ctx context.Context, r *FoundationDBClusterReconciler,
 		incorrectPod = !updated
 	}
 
-	processGroupStatus.UpdateCondition(fdbv1beta2.IncorrectPodSpec, incorrectPod, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.IncorrectPodSpec, incorrectPod)
 
 	// If we do a cluster version incompatible upgrade we use the fdbv1beta2.IncorrectConfigMap to signal when the operator
 	// can restart fdbserver processes. Since the ConfigMap itself won't change during the upgrade we have to run the updatePodDynamicConf
@@ -589,7 +589,7 @@ func validateProcessGroup(ctx context.Context, r *FoundationDBClusterReconciler,
 		synced = pod.ObjectMeta.Annotations[fdbv1beta2.LastConfigMapKey] == configMapHash
 	}
 
-	processGroupStatus.UpdateCondition(fdbv1beta2.IncorrectConfigMap, !synced, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.IncorrectConfigMap, !synced)
 
 	desiredPvc, err := internal.GetPvc(cluster, processGroupStatus.ProcessClass, idNum)
 	if err != nil {
@@ -604,10 +604,10 @@ func validateProcessGroup(ctx context.Context, r *FoundationDBClusterReconciler,
 		log.Info("ValidateProcessGroup found incorrectPVC", "CurrentPVC", currentPVC, "DesiredPVC", desiredPvc)
 	}
 
-	processGroupStatus.UpdateCondition(fdbv1beta2.MissingPVC, incorrectPVC, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.MissingPVC, incorrectPVC)
 
 	if pod.Status.Phase == corev1.PodPending {
-		processGroupStatus.UpdateCondition(fdbv1beta2.PodPending, true, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+		processGroupStatus.UpdateCondition(fdbv1beta2.PodPending, true)
 		return nil
 	}
 
@@ -638,8 +638,8 @@ func validateProcessGroup(ctx context.Context, r *FoundationDBClusterReconciler,
 		}
 	}
 
-	processGroupStatus.UpdateCondition(fdbv1beta2.PodFailing, failing, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
-	processGroupStatus.UpdateCondition(fdbv1beta2.PodPending, false, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+	processGroupStatus.UpdateCondition(fdbv1beta2.PodFailing, failing)
+	processGroupStatus.UpdateCondition(fdbv1beta2.PodPending, false)
 
 	if !disableTaintFeature {
 		// Update taint status
@@ -692,14 +692,14 @@ func updateTaintCondition(ctx context.Context, r *FoundationDBClusterReconciler,
 			}
 
 			hasMatchingTaint = true
-			processGroupStatus.UpdateCondition(fdbv1beta2.NodeTaintDetected, true, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+			processGroupStatus.UpdateCondition(fdbv1beta2.NodeTaintDetected, true)
 			// Use node taint's timestamp as the NodeTaintDetected condition's starting time
 			if !taint.TimeAdded.IsZero() && taint.TimeAdded.Time.Unix() < *processGroupStatus.GetConditionTime(fdbv1beta2.NodeTaintDetected) {
 				processGroupStatus.UpdateConditionTime(fdbv1beta2.NodeTaintDetected, taint.TimeAdded.Unix())
 			}
 			taintDetectedTime := pointer.Int64Deref(processGroupStatus.GetConditionTime(fdbv1beta2.NodeTaintDetected), math.MaxInt64)
 			if time.Now().Unix()-taintDetectedTime > pointer.Int64Deref(taintConfiguredKey.DurationInSeconds, math.MaxInt64) {
-				processGroupStatus.UpdateCondition(fdbv1beta2.NodeTaintReplacing, true, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+				processGroupStatus.UpdateCondition(fdbv1beta2.NodeTaintReplacing, true)
 				logger.Info("Add NodeTaintReplacing condition", "Pod", pod.Name, "Node", node.Name,
 					"TaintKey", taint.Key, "TaintDetectedTime", taintDetectedTime,
 					"TaintDuration", int64(time.Since(time.Unix(taintDetectedTime, 0))),
@@ -712,47 +712,12 @@ func updateTaintCondition(ctx context.Context, r *FoundationDBClusterReconciler,
 		// Remove NodeTaintDetected condition if the pod is no longer on a tainted node;
 		// This is needed especially when a node's status is flapping
 		// We do not remove NodeTaintDetected because the NodeTaintDetected condition is only added when taint is there for configured long time
-		processGroupStatus.UpdateCondition(fdbv1beta2.NodeTaintDetected, false, cluster.Status.ProcessGroups, processGroupStatus.ProcessGroupID)
+		processGroupStatus.UpdateCondition(fdbv1beta2.NodeTaintDetected, false)
 	}
 
 	return nil
 }
 
-// removeDuplicateConditions will remove all duplicated conditions from the status and if a process group has the ResourcesTerminating
-// condition it will remove all other conditions on that process group.
-func removeDuplicateConditions(status fdbv1beta2.FoundationDBClusterStatus) {
-	for _, processGroupStatus := range status.ProcessGroups {
-		conditionTimes := make(map[fdbv1beta2.ProcessGroupConditionType]int64, len(processGroupStatus.ProcessGroupConditions))
-		copiedConditions := make(map[fdbv1beta2.ProcessGroupConditionType]bool, len(processGroupStatus.ProcessGroupConditions))
-		conditions := make([]*fdbv1beta2.ProcessGroupCondition, 0, len(processGroupStatus.ProcessGroupConditions))
-		isTerminating := false
-
-		for _, condition := range processGroupStatus.ProcessGroupConditions {
-			existingTime, present := conditionTimes[condition.ProcessGroupConditionType]
-			if !present || existingTime > condition.Timestamp {
-				conditionTimes[condition.ProcessGroupConditionType] = condition.Timestamp
-			}
-
-			if condition.ProcessGroupConditionType == fdbv1beta2.ResourcesTerminating {
-				isTerminating = true
-			}
-		}
-
-		for _, condition := range processGroupStatus.ProcessGroupConditions {
-			if condition.Timestamp == conditionTimes[condition.ProcessGroupConditionType] && !copiedConditions[condition.ProcessGroupConditionType] {
-				if isTerminating && condition.ProcessGroupConditionType != fdbv1beta2.ResourcesTerminating {
-					continue
-				}
-
-				conditions = append(conditions, condition)
-				copiedConditions[condition.ProcessGroupConditionType] = true
-			}
-		}
-
-		processGroupStatus.ProcessGroupConditions = conditions
-	}
-}
-
 func refreshProcessGroupStatus(ctx context.Context, r *FoundationDBClusterReconciler, cluster *fdbv1beta2.FoundationDBCluster, status *fdbv1beta2.FoundationDBClusterStatus) (*corev1.PersistentVolumeClaimList, error) {
 	status.ProcessGroups = make([]*fdbv1beta2.ProcessGroupStatus, 0, len(cluster.Status.ProcessGroups))
 	for _, processGroup := range cluster.Status.ProcessGroups {


Test patch:
diff --git a/api/v1beta2/foundationdbcluster_types_test.go b/api/v1beta2/foundationdbcluster_types_test.go
index 4dcf55f4..18188b84 100644
--- a/api/v1beta2/foundationdbcluster_types_test.go
+++ b/api/v1beta2/foundationdbcluster_types_test.go
@@ -3090,7 +3090,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 				cluster = createCluster()
 				cluster.Spec.ProcessCounts.Storage = 2
 				cluster.Status.ProcessGroups[0].MarkForRemoval()
-				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true, nil, "")
+				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true)
 				result, err = cluster.CheckReconciliation(log)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(result).To(BeTrue())
@@ -3103,8 +3103,8 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 				cluster.Spec.ProcessCounts.Storage = 2
 				cluster.Status.ProcessGroups[0].MarkForRemoval()
 				cluster.Status.ProcessGroups[0].SetExclude()
-				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true, nil, "")
-				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true, nil, "")
+				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true)
+				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true)
 				result, err = cluster.CheckReconciliation(log)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(result).To(BeTrue())
@@ -3134,7 +3134,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 				}))
 
 				cluster = createCluster()
-				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true, nil, "")
+				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true)
 				result, err = cluster.CheckReconciliation(log)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(result).To(BeFalse())
@@ -3342,7 +3342,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 				cluster = createCluster()
 				cluster.Spec.ProcessCounts.Storage = 2
 				cluster.Status.ProcessGroups[0].MarkForRemoval()
-				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true, nil, "")
+				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true)
 				result, err = cluster.CheckReconciliation(log)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(result).To(BeTrue())
@@ -3355,8 +3355,8 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 				cluster.Spec.ProcessCounts.Storage = 2
 				cluster.Status.ProcessGroups[0].MarkForRemoval()
 				cluster.Status.ProcessGroups[0].SetExclude()
-				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true, nil, "")
-				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true, nil, "")
+				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true)
+				cluster.Status.ProcessGroups[0].UpdateCondition(ResourcesTerminating, true)
 				result, err = cluster.CheckReconciliation(log)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(result).To(BeTrue())
@@ -3386,7 +3386,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 				}))
 
 				cluster = createCluster()
-				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true, nil, "")
+				cluster.Status.ProcessGroups[0].UpdateCondition(IncorrectCommandLine, true)
 				result, err = cluster.CheckReconciliation(log)
 				Expect(err).NotTo(HaveOccurred())
 				Expect(result).To(BeFalse())
@@ -4046,7 +4046,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 
 		Context("with a process group that went missing after the window", func() {
 			BeforeEach(func() {
-				processGroup.UpdateCondition(MissingProcesses, true, nil, "")
+				processGroup.UpdateCondition(MissingProcesses, true)
 			})
 
 			It("should not need replacement", func() {
@@ -4057,7 +4057,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 		Context("with a process group that went missing before the window", func() {
 			var targetTimestamp int64
 			BeforeEach(func() {
-				processGroup.UpdateCondition(MissingProcesses, true, nil, "")
+				processGroup.UpdateCondition(MissingProcesses, true)
 				targetTimestamp = time.Now().Add(-1 * time.Hour).Unix()
 				processGroup.ProcessGroupConditions[0].Timestamp = targetTimestamp
 			})
@@ -4071,8 +4071,8 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 		Context("with multiple conditions that could trigger a replacement", func() {
 			var targetTimestamp int64
 			BeforeEach(func() {
-				processGroup.UpdateCondition(MissingProcesses, true, nil, "")
-				processGroup.UpdateCondition(PodFailing, true, nil, "")
+				processGroup.UpdateCondition(MissingProcesses, true)
+				processGroup.UpdateCondition(PodFailing, true)
 				targetTimestamp = time.Now().Add(-1 * time.Hour).Unix()
 				processGroup.ProcessGroupConditions[0].Timestamp = targetTimestamp
 				processGroup.ProcessGroupConditions[1].Timestamp = targetTimestamp - 60
@@ -4086,7 +4086,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 
 		Context("with a process group that failed", func() {
 			BeforeEach(func() {
-				processGroup.UpdateCondition(PodFailing, true, nil, "")
+				processGroup.UpdateCondition(PodFailing, true)
 				processGroup.ProcessGroupConditions[0].Timestamp = oldTimestamp
 			})
 
@@ -4098,7 +4098,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 
 		When("process group is in the missing Pod state", func() {
 			BeforeEach(func() {
-				processGroup.UpdateCondition(MissingPod, true, nil, "")
+				processGroup.UpdateCondition(MissingPod, true)
 				processGroup.ProcessGroupConditions[0].Timestamp = oldTimestamp
 			})
 
@@ -4110,7 +4110,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 
 		When("process group is in the missing PVC state", func() {
 			BeforeEach(func() {
-				processGroup.UpdateCondition(MissingPVC, true, nil, "")
+				processGroup.UpdateCondition(MissingPVC, true)
 				processGroup.ProcessGroupConditions[0].Timestamp = oldTimestamp
 			})
 
@@ -4122,7 +4122,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 
 		When("process group is in the missing Service state", func() {
 			BeforeEach(func() {
-				processGroup.UpdateCondition(MissingService, true, nil, "")
+				processGroup.UpdateCondition(MissingService, true)
 				processGroup.ProcessGroupConditions[0].Timestamp = oldTimestamp
 			})
 
@@ -4134,7 +4134,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 
 		When("process group is in the Pod pending state", func() {
 			BeforeEach(func() {
-				processGroup.UpdateCondition(PodPending, true, nil, "")
+				processGroup.UpdateCondition(PodPending, true)
 				processGroup.ProcessGroupConditions[0].Timestamp = oldTimestamp
 			})
 
@@ -4146,7 +4146,7 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 
 		Context("with a process group that had the wrong command line", func() {
 			BeforeEach(func() {
-				processGroup.UpdateCondition(IncorrectCommandLine, true, nil, "")
+				processGroup.UpdateCondition(IncorrectCommandLine, true)
 				processGroup.ProcessGroupConditions[0].Timestamp = oldTimestamp
 			})
 
@@ -5066,4 +5066,108 @@ var _ = Describe("[api] FoundationDBCluster", func() {
 			},
 			"testing-cluster-cluster-controller-1"),
 	)
+
+	DescribeTable("when adding a condition to a process group", func(processGroup *ProcessGroupStatus, condition ProcessGroupConditionType, expectedConditions []*ProcessGroupCondition) {
+		processGroup.addCondition(condition)
+
+		Expect(processGroup.ProcessGroupConditions).To(HaveLen(len(expectedConditions)))
+		for _, expectedCondition := range expectedConditions {
+			timestamp := processGroup.GetConditionTime(expectedCondition.ProcessGroupConditionType)
+			Expect(timestamp).NotTo(BeNil())
+			if expectedCondition.Timestamp > 0 {
+				Expect(*timestamp).To(BeNumerically("==", expectedCondition.Timestamp))
+			}
+		}
+	}, Entry("no conditions are present",
+		&ProcessGroupStatus{},
+		PodPending,
+		[]*ProcessGroupCondition{
+			{
+				ProcessGroupConditionType: PodPending,
+			},
+		}),
+		Entry("adding a duplicate condition",
+			&ProcessGroupStatus{
+				ProcessGroupConditions: []*ProcessGroupCondition{
+					{
+						ProcessGroupConditionType: PodPending,
+						Timestamp:                 15,
+					},
+				},
+			},
+			PodPending,
+			[]*ProcessGroupCondition{
+				{
+					ProcessGroupConditionType: PodPending,
+					Timestamp:                 15,
+				},
+			}),
+		Entry("adding a new condition",
+			&ProcessGroupStatus{
+				ProcessGroupConditions: []*ProcessGroupCondition{
+					{
+						ProcessGroupConditionType: PodPending,
+					},
+				},
+			},
+			PodFailing,
+			[]*ProcessGroupCondition{
+				{
+					ProcessGroupConditionType: PodPending,
+				},
+				{
+					ProcessGroupConditionType: PodFailing,
+				},
+			}),
+		Entry("adding the resource terminating condition to process group marked as removed and excluded",
+			&ProcessGroupStatus{
+				RemovalTimestamp: &metav1.Time{
+					Time: time.Now(),
+				},
+				ExclusionSkipped: true,
+			},
+			ResourcesTerminating,
+			[]*ProcessGroupCondition{
+				{
+					ProcessGroupConditionType: ResourcesTerminating,
+				},
+			}),
+		Entry("adding another condition to process group marked as removed and excluded",
+			&ProcessGroupStatus{
+				RemovalTimestamp: &metav1.Time{
+					Time: time.Now(),
+				},
+				ExclusionSkipped: true,
+			},
+			PodPending,
+			[]*ProcessGroupCondition{}),
+	)
+
+	DescribeTable("when marking a process group as excluded", func(processGroup *ProcessGroupStatus) {
+		processGroup.SetExclude()
+
+		Expect(processGroup.ProcessGroupConditions).To(HaveLen(0))
+		Expect(processGroup.ExclusionTimestamp.IsZero()).To(BeFalse())
+	}, Entry("no conditions are present",
+		&ProcessGroupStatus{}),
+		Entry("one condition is present",
+			&ProcessGroupStatus{
+				ProcessGroupConditions: []*ProcessGroupCondition{
+					{
+						ProcessGroupConditionType: PodPending,
+					},
+				},
+			}),
+		Entry("multiple conditions are present",
+			&ProcessGroupStatus{
+				ProcessGroupConditions: []*ProcessGroupCondition{
+					{
+						ProcessGroupConditionType: PodPending,
+					},
+					{
+						ProcessGroupConditionType: PodFailing,
+					},
+				},
+			}),
+	)
 })
diff --git a/controllers/bounce_processes_test.go b/controllers/bounce_processes_test.go
index cc8865ff..3621212d 100644
--- a/controllers/bounce_processes_test.go
+++ b/controllers/bounce_processes_test.go
@@ -74,11 +74,11 @@ var _ = Describe("bounceProcesses", func() {
 		BeforeEach(func() {
 			processGroup := cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-4]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-1")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 
 			processGroup = cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-3]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-2")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 		})
 
 		It("should not requeue", func() {
@@ -101,11 +101,11 @@ var _ = Describe("bounceProcesses", func() {
 		BeforeEach(func() {
 			processGroup := cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-4]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-1")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 
 			processGroup = cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-3]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-2")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 			processGroup.MarkForRemoval()
 		})
 
@@ -129,7 +129,7 @@ var _ = Describe("bounceProcesses", func() {
 		BeforeEach(func() {
 			processGroup := cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-4]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-1")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 			for _, address := range processGroup.Addresses {
 				err := adminClient.ExcludeProcesses([]fdbv1beta2.ProcessAddress{{StringAddress: address, Port: 4501}})
 				Expect(err).To(BeNil())
@@ -160,8 +160,8 @@ var _ = Describe("bounceProcesses", func() {
 		BeforeEach(func() {
 			processGroup := cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-4]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-1")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
-			processGroup.UpdateCondition(fdbv1beta2.PodPending, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
+			processGroup.UpdateCondition(fdbv1beta2.PodPending, true)
 			cluster.Spec.AutomationOptions.IgnorePendingPodsDuration = 1 * time.Nanosecond
 		})
 
@@ -182,7 +182,7 @@ var _ = Describe("bounceProcesses", func() {
 		BeforeEach(func() {
 			processGroup := cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-4]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-1")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 			processGroup.ProcessGroupConditions = append(processGroup.ProcessGroupConditions, &fdbv1beta2.ProcessGroupCondition{
 				ProcessGroupConditionType: fdbv1beta2.MissingProcesses,
 				Timestamp:                 time.Now().Add(-2 * time.Minute).Unix(),
@@ -190,7 +190,7 @@ var _ = Describe("bounceProcesses", func() {
 
 			processGroup = cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-3]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-2")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 		})
 
 		It("should not requeue", func() {
@@ -219,11 +219,11 @@ var _ = Describe("bounceProcesses", func() {
 
 			processGroup := cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-4]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-5")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 
 			processGroup = cluster.Status.ProcessGroups[len(cluster.Status.ProcessGroups)-3]
 			Expect(processGroup.ProcessGroupID).To(Equal(fdbv1beta2.ProcessGroupID("storage-6")))
-			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 		})
 
 		It("should not requeue", func() {
@@ -246,7 +246,7 @@ var _ = Describe("bounceProcesses", func() {
 			BeforeEach(func() {
 				cluster.Spec.Version = fdbv1beta2.Versions.NextMajorVersion.String()
 				for _, processGroup := range cluster.Status.ProcessGroups {
-					processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+					processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 				}
 			})
 
@@ -289,7 +289,7 @@ var _ = Describe("bounceProcesses", func() {
 		BeforeEach(func() {
 			cluster.Spec.Version = fdbv1beta2.Versions.NextMajorVersion.String()
 			for _, processGroup := range cluster.Status.ProcessGroups {
-				processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+				processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 			}
 		})
 
@@ -428,7 +428,7 @@ var _ = Describe("bounceProcesses", func() {
 			BeforeEach(func() {
 				missingProcessGroup = cluster.Status.ProcessGroups[0]
 				adminClient.MockMissingProcessGroup(missingProcessGroup.ProcessGroupID, true)
-				missingProcessGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true, cluster.Status.ProcessGroups, missingProcessGroup.ProcessGroupID)
+				missingProcessGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true)
 			})
 
 			It("should requeue", func() {
@@ -486,7 +486,7 @@ var _ = Describe("bounceProcesses", func() {
 			ignoredProcessGroup = cluster.Status.ProcessGroups[0]
 			cluster.Spec.Buggify.IgnoreDuringRestart = []fdbv1beta2.ProcessGroupID{ignoredProcessGroup.ProcessGroupID}
 			for _, processGroup := range cluster.Status.ProcessGroups {
-				processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true, nil, "")
+				processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, true)
 			}
 		})
 
@@ -532,7 +532,7 @@ var _ = Describe("bounceProcesses", func() {
 
 			BeforeEach(func() {
 				for _, processGroup := range cluster.Status.ProcessGroups {
-					processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, processGroup.ProcessGroupID == ignoredProcessGroup.ProcessGroupID, nil, "")
+					processGroup.UpdateCondition(fdbv1beta2.IncorrectCommandLine, processGroup.ProcessGroupID == ignoredProcessGroup.ProcessGroupID)
 				}
 
 				status, err := adminClient.GetStatus()
diff --git a/controllers/cluster_controller_test.go b/controllers/cluster_controller_test.go
index 1da92acb..8d498f28 100644
--- a/controllers/cluster_controller_test.go
+++ b/controllers/cluster_controller_test.go
@@ -638,7 +638,7 @@ var _ = Describe("cluster_controller", func() {
 						pod := originalPods.Items[firstStorageIndex]
 						for _, processGroup := range cluster.Status.ProcessGroups {
 							if processGroup.ProcessGroupID == fdbv1beta2.ProcessGroupID(pod.ObjectMeta.Labels[fdbv1beta2.FDBProcessGroupIDLabel]) {
-								processGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true, nil, "")
+								processGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true)
 							}
 						}
 
@@ -678,7 +678,7 @@ var _ = Describe("cluster_controller", func() {
 						originalPod := &originalPods.Items[firstStorageIndex]
 						for _, processGroup := range cluster.Status.ProcessGroups {
 							if processGroup.ProcessGroupID == fdbv1beta2.ProcessGroupID(originalPod.ObjectMeta.Labels[fdbv1beta2.FDBProcessGroupIDLabel]) {
-								processGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true, nil, "")
+								processGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true)
 							}
 						}
 
diff --git a/controllers/exclude_processes_test.go b/controllers/exclude_processes_test.go
index 50ce3e0d..e60661fd 100644
--- a/controllers/exclude_processes_test.go
+++ b/controllers/exclude_processes_test.go
@@ -356,7 +356,7 @@ func createMissingProcesses(cluster *fdbv1beta2.FoundationDBCluster, count int,
 	missing := 0
 	for _, processGroup := range cluster.Status.ProcessGroups {
 		if processGroup.ProcessClass == processClass {
-			processGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true, nil, "")
+			processGroup.UpdateCondition(fdbv1beta2.MissingProcesses, true)
 			missing++
 			if missing == count {
 				break
diff --git a/controllers/remove_process_groups_test.go b/controllers/remove_process_groups_test.go
index 705955d0..6f037db9 100644
--- a/controllers/remove_process_groups_test.go
+++ b/controllers/remove_process_groups_test.go
@@ -23,8 +23,6 @@ package controllers
 import (
 	"context"
 	"fmt"
-	"time"
-
 	"github.com/FoundationDB/fdb-kubernetes-operator/pkg/fdbadminclient/mock"
 
 	"k8s.io/utils/pointer"
@@ -220,8 +218,6 @@ var _ = Describe("remove_process_groups", func() {
 						secondRemovedProcessGroup.ProcessGroupConditions = append(secondRemovedProcessGroup.ProcessGroupConditions, fdbv1beta2.NewProcessGroupCondition(fdbv1beta2.ResourcesTerminating))
 						err := removeProcessGroup(context.Background(), clusterReconciler, cluster, secondRemovedProcessGroup.ProcessGroupID)
 						Expect(err).NotTo(HaveOccurred())
-						// Sleep here to prevent some timeing issues.
-						time.Sleep(10 * time.Microsecond)
 					})
 
 					It("should remove the process group and the terminated process group", func() {
@@ -243,8 +239,6 @@ var _ = Describe("remove_process_groups", func() {
 				When("a process group is marked as terminating and the resources are not removed", func() {
 					BeforeEach(func() {
 						secondRemovedProcessGroup.ProcessGroupConditions = append(secondRemovedProcessGroup.ProcessGroupConditions, fdbv1beta2.NewProcessGroupCondition(fdbv1beta2.ResourcesTerminating))
-						// Sleep here to prevent some timeing issues.
-						time.Sleep(10 * time.Microsecond)
 					})
 
 					It("should remove the process group and the terminated process group", func() {
diff --git a/controllers/replace_failed_process_groups_test.go b/controllers/replace_failed_process_groups_test.go
index 3d5cc56e..0075f67a 100644
--- a/controllers/replace_failed_process_groups_test.go
+++ b/controllers/replace_failed_process_groups_test.go
@@ -373,7 +373,7 @@ var _ = Describe("replace_failed_process_groups", func() {
 				_, err = reconcileCluster(cluster)
 				Expect(err).NotTo(HaveOccurred())
 				return (getPodByProcessGroupID(cluster, internal.GetProcessClassFromMeta(cluster, podOnTaintedNode.ObjectMeta), internal.GetProcessGroupIDFromMeta(cluster, podOnTaintedNode.ObjectMeta)))
-			}).WithTimeout(time.Duration(cluster.GetTaintReplacementTimeSeconds()*3) * time.Second).WithPolling(1 * time.Second).Should(BeNil())
+			}).WithTimeout(time.Duration(cluster.GetTaintReplacementTimeSeconds()*10) * time.Second).WithPolling(1 * time.Second).Should(BeNil())
 
 			Expect(getRemovedProcessGroupIDs(cluster)).To(Equal([]fdbv1beta2.ProcessGroupID{}))
 		})
diff --git a/controllers/update_status_test.go b/controllers/update_status_test.go
index f6a4f3c9..328b0415 100644
--- a/controllers/update_status_test.go
+++ b/controllers/update_status_test.go
@@ -666,56 +666,6 @@ var _ = Describe("update_status", func() {
 		})
 	})
 
-	When("removing duplicated entries in process group status", func() {
-		var status fdbv1beta2.FoundationDBClusterStatus
-
-		BeforeEach(func() {
-			status = fdbv1beta2.FoundationDBClusterStatus{
-				ProcessGroups: []*fdbv1beta2.ProcessGroupStatus{
-					{
-						ProcessGroupID: storageOneProcessGroupID,
-						ProcessGroupConditions: []*fdbv1beta2.ProcessGroupCondition{
-							fdbv1beta2.NewProcessGroupCondition(fdbv1beta2.PodPending),
-							fdbv1beta2.NewProcessGroupCondition(fdbv1beta2.ResourcesTerminating),
-						},
-					},
-					{
-						ProcessGroupID: "storage-2",
-						ProcessGroupConditions: []*fdbv1beta2.ProcessGroupCondition{
-							fdbv1beta2.NewProcessGroupCondition(fdbv1beta2.PodPending),
-							fdbv1beta2.NewProcessGroupCondition(fdbv1beta2.PodPending),
-							fdbv1beta2.NewProcessGroupCondition(fdbv1beta2.SidecarUnreachable),
-						},
-					},
-				},
-			}
-		})
-
-		When("running the remove duplicate method", func() {
-			BeforeEach(func() {
-				removeDuplicateConditions(status)
-			})
-
-			It("should remove duplicated entries", func() {
-				Expect(len(status.ProcessGroups)).To(BeNumerically("==", 2))
-
-				for _, pg := range status.ProcessGroups {
-					if pg.ProcessGroupID == storageOneProcessGroupID {
-						Expect(len(pg.ProcessGroupConditions)).To(BeNumerically("==", 1))
-						continue
-					}
-
-					if pg.ProcessGroupID == "storage-2" {
-						Expect(len(pg.ProcessGroupConditions)).To(BeNumerically("==", 2))
-						continue
-					}
-
-					Fail("unchecked process group")
-				}
-			})
-		})
-	})
-
 	Describe("Reconcile", func() {
 		var cluster *fdbv1beta2.FoundationDBCluster
 		var err error
diff --git a/e2e/README.md b/e2e/README.md
index b43cc658..87f3dcc0 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -48,7 +48,7 @@ If those tests are running on a cluster that has no chaos-mesh installed, you ca
 ### Running e2e tests in kind
 
 [kind](https://kind.sigs.k8s.io) provides an easy way to run a local Kubernetes cluster.
-The following steps assume that `kind` is already installed.
+The following steps assume that `kind` and `helm` are already installed.
 
 ```bash
 make -C e2e kind-setup