keep deployment paused on rollout deletion behind feature‑gate

Signed-off-by: GautamBytes <manchandanigautam@gmail.com>
This commit is contained in:
GautamBytes 2025-06-21 07:13:41 +00:00
parent deaa5f38b5
commit 79bc354eea
5 changed files with 150 additions and 3 deletions

View File

@ -44,6 +44,9 @@ const (
// RollbackInBatchAnnotation is set to rollout annotations.
// RollbackInBatchAnnotation allow use disable quick rollback, and will roll back in batch style.
RollbackInBatchAnnotation = "rollouts.kruise.io/rollback-in-batch"
// RolloutFinalizer is the finalizer string that the Rollout controller adds to Rollout objects.
RolloutFinalizer = "rollouts.kruise.io/finalizer"
)
// RolloutSpec defines the desired state of Rollout

View File

@ -21,10 +21,13 @@ import (
"fmt"
"reflect"
"time"
apps "k8s.io/api/apps/v1"
"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/api/v1beta1"
"github.com/openkruise/rollouts/pkg/trafficrouting"
"github.com/openkruise/rollouts/pkg/feature"
utilfeature "github.com/openkruise/rollouts/pkg/util/feature"
"github.com/openkruise/rollouts/pkg/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@ -305,7 +308,27 @@ func (m *blueGreenReleaseManager) doCanaryFinalising(c *RolloutContext) (bool, e
switch blueGreenStatus.FinalisingStep {
// set workload.pause=false; set workload.partition=0
case v1beta1.FinalisingStepResumeWorkload:
retry, err = finalizingBatchRelease(m.Client, c)
// If the rollout is being deleted and the feature gate is enabled,
// we skip this step entirely to keep the deployment paused.
if c.FinalizeReason == v1beta1.FinaliseReasonDelete && utilfeature.DefaultMutableFeatureGate.Enabled(feature.KeepDeploymentPausedOnDeletionGate) {
klog.Infof("Rollout(%s/%s) is being deleted, KeepDeploymentPausedOnDeletion is enabled. Skipping resume workload step.", c.Rollout.Namespace, c.Rollout.Name)
// Do nothing, effectively skipping this step and considering it "done".
} else {
// Otherwise, run the original logic to unpause the workload.
retry, err = finalizingBatchRelease(m.Client, c)
if err == nil && !retry {
dep := &apps.Deployment{}
key := client.ObjectKey{Namespace: c.Rollout.Namespace, Name: c.Rollout.Spec.WorkloadRef.Name}
if getErr := m.Client.Get(context.TODO(), key, dep); getErr == nil {
if dep.Spec.Paused {
dep.Spec.Paused = false
if updErr := m.Client.Update(context.TODO(), dep); updErr != nil {
return false, updErr
}
}
}
}
}
// delete batchRelease
case v1beta1.FinalisingStepReleaseWorkloadControl:
retry, err = removeBatchRelease(m.Client, c)

View File

@ -21,10 +21,12 @@ import (
"fmt"
"reflect"
"time"
apps "k8s.io/api/apps/v1"
"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/api/v1beta1"
"github.com/openkruise/rollouts/pkg/trafficrouting"
"github.com/openkruise/rollouts/pkg/feature"
utilfeature "github.com/openkruise/rollouts/pkg/util/feature"
"github.com/openkruise/rollouts/pkg/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@ -371,7 +373,27 @@ func (m *canaryReleaseManager) doCanaryFinalising(c *RolloutContext) (bool, erro
switch canaryStatus.FinalisingStep {
// set workload.pause=false; set workload.partition=0
case v1beta1.FinalisingStepResumeWorkload:
retry, err = finalizingBatchRelease(m.Client, c)
// If the rollout is being deleted and the feature gate is enabled,
// we skip this step entirely to keep the deployment paused.
if c.FinalizeReason == v1beta1.FinaliseReasonDelete && utilfeature.DefaultMutableFeatureGate.Enabled(feature.KeepDeploymentPausedOnDeletionGate) {
klog.Infof("Rollout(%s/%s) is being deleted, KeepDeploymentPausedOnDeletion is enabled. Skipping resume workload step.", c.Rollout.Namespace, c.Rollout.Name)
// Do nothing, effectively skipping this step and considering it "done".
} else {
// Otherwise, run the original logic to unpause the workload.
retry, err = finalizingBatchRelease(m.Client, c)
if err == nil && !retry {
dep := &apps.Deployment{}
key := client.ObjectKey{Namespace: c.Rollout.Namespace, Name: c.Rollout.Spec.WorkloadRef.Name}
if getErr := m.Client.Get(context.TODO(), key, dep); getErr == nil {
if dep.Spec.Paused {
dep.Spec.Paused = false
if updErr := m.Client.Update(context.TODO(), dep); updErr != nil {
return false, updErr
}
}
}
}
}
// delete batchRelease
case v1beta1.FinalisingStepReleaseWorkloadControl:
retry, err = removeBatchRelease(m.Client, c)

View File

@ -17,10 +17,20 @@ limitations under the License.
package rollout
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/openkruise/rollouts/pkg/trafficrouting"
"fmt"
rolloutapi "github.com/openkruise/rollouts/api"
"github.com/openkruise/rollouts/api/v1alpha1"
"github.com/openkruise/rollouts/pkg/feature"
utilfeature "github.com/openkruise/rollouts/pkg/util/feature"
"github.com/openkruise/rollouts/api/v1beta1"
"github.com/openkruise/rollouts/pkg/util"
"github.com/openkruise/rollouts/pkg/util/configuration"
@ -420,3 +430,89 @@ func init() {
_ = clientgoscheme.AddToScheme(scheme)
_ = rolloutapi.AddToScheme(scheme)
}
func TestFinalizeRolloutKeepPaused(t *testing.T) {
// The scheme is already initialized in the file's init() function.
deployment := deploymentDemo.DeepCopy()
// Simulate the deployment being paused by the rollout controller
deployment.Spec.Paused = true
rollout := rolloutDemo.DeepCopy()
// Set the rollout to a finalizing state
rollout.Status.Phase = v1beta1.RolloutPhaseDisabling
rollout.Status.CanaryStatus = &v1beta1.CanaryStatus{}
now := metav1.Now()
rollout.DeletionTimestamp = &now
// Use the correct finalizer constant from the util package.
rollout.Finalizers = []string{v1beta1.RolloutFinalizer}
// Test Case 1: Feature Gate is ENABLED
t.Run("Deployment should remain paused when gate is enabled", func(t *testing.T) {
// Enable the feature gate for this specific test
err := utilfeature.DefaultMutableFeatureGate.Set(string(feature.KeepDeploymentPausedOnDeletionGate) + "=true")
assert.Nil(t, err)
// Ensure the feature gate is reset after the test
defer utilfeature.DefaultMutableFeatureGate.Set(string(feature.KeepDeploymentPausedOnDeletionGate) + "=false")
// The reconciler needs a fake client with the test objects
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(rollout.DeepCopy(), deployment.DeepCopy()).Build()
rollout.Status.CanaryStatus.FinalisingStep = v1beta1.FinalisingStepResumeWorkload
r := &RolloutReconciler{
Client: fakeClient,
Scheme: scheme,
finder: util.NewControllerFinder(fakeClient),
canaryManager: &canaryReleaseManager{
Client: fakeClient,
trafficRoutingManager: trafficrouting.NewTrafficRoutingManager(fakeClient),
},
blueGreenManager: &blueGreenReleaseManager{
Client: fakeClient,
trafficRoutingManager: trafficrouting.NewTrafficRoutingManager(fakeClient),
},
}
// Run the reconcile loop to trigger the finalizer
_, err = r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: client.ObjectKeyFromObject(rollout)})
assert.Nil(t, err)
// Verify the deployment is still paused
updatedDeployment := &apps.Deployment{}
err = r.Get(context.TODO(), client.ObjectKeyFromObject(deployment), updatedDeployment)
assert.Nil(t, err)
assert.True(t, updatedDeployment.Spec.Paused, "Deployment should have remained paused")
})
// Test Case 2: Feature Gate is DISABLED (Default Behavior)
t.Run("Deployment should be unpaused when gate is disabled", func(t *testing.T) {
// Ensure the feature gate is disabled
err := utilfeature.DefaultMutableFeatureGate.Set(string(feature.KeepDeploymentPausedOnDeletionGate) + "=false")
assert.Nil(t, err)
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(rollout.DeepCopy(), deployment.DeepCopy()).Build()
rollout.Status.CanaryStatus.FinalisingStep = v1beta1.FinalisingStepResumeWorkload
r := &RolloutReconciler{
Client: fakeClient,
Scheme: scheme,
finder: util.NewControllerFinder(fakeClient),
canaryManager: &canaryReleaseManager{
Client: fakeClient,
trafficRoutingManager: trafficrouting.NewTrafficRoutingManager(fakeClient),
},
blueGreenManager: &blueGreenReleaseManager{
Client: fakeClient,
trafficRoutingManager: trafficrouting.NewTrafficRoutingManager(fakeClient),
},
}
// Run the reconcile loop
_, err = r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: client.ObjectKeyFromObject(rollout)})
assert.Nil(t, err)
// Verify the deployment is now unpaused (reverted to original spec)
updatedDeployment := &apps.Deployment{}
err = r.Get(context.TODO(), client.ObjectKeyFromObject(deployment), updatedDeployment)
assert.Nil(t, err)
assert.False(t, updatedDeployment.Spec.Paused, "Deployment should have been unpaused")
})
}

View File

@ -30,12 +30,15 @@ const (
AdvancedDeploymentGate featuregate.Feature = "AdvancedDeployment"
// AppendServiceSelectorGate enable appending pod labels from PodTemplateMetadata to the canary service selector.
AppendServiceSelectorGate featuregate.Feature = "AppendPodSelector"
// KeepDeploymentPausedOnDeletionGate prevents unpausing a Deployment when its managing Rollout CR is deleted.
KeepDeploymentPausedOnDeletionGate featuregate.Feature = "KeepDeploymentPausedOnDeletion"
)
var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
RolloutHistoryGate: {Default: false, PreRelease: featuregate.Alpha},
AdvancedDeploymentGate: {Default: false, PreRelease: featuregate.Alpha},
AppendServiceSelectorGate: {Default: false, PreRelease: featuregate.Alpha},
KeepDeploymentPausedOnDeletionGate: {Default: false, PreRelease: featuregate.Alpha},
}
func init() {