k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controller/deployment/progress_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package deployment
    18  
    19  import (
    20  	"context"
    21  	"math"
    22  	"testing"
    23  	"time"
    24  
    25  	apps "k8s.io/api/apps/v1"
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/client-go/kubernetes/fake"
    29  	"k8s.io/client-go/util/workqueue"
    30  	"k8s.io/klog/v2/ktesting"
    31  	"k8s.io/kubernetes/pkg/controller/deployment/util"
    32  )
    33  
    34  func newDeploymentStatus(replicas, updatedReplicas, availableReplicas int32) apps.DeploymentStatus {
    35  	return apps.DeploymentStatus{
    36  		Replicas:          replicas,
    37  		UpdatedReplicas:   updatedReplicas,
    38  		AvailableReplicas: availableReplicas,
    39  	}
    40  }
    41  
    42  // assumes the retuned deployment is always observed - not needed to be tested here.
    43  func currentDeployment(pds *int32, replicas, statusReplicas, updatedReplicas, availableReplicas int32, conditions []apps.DeploymentCondition) *apps.Deployment {
    44  	d := &apps.Deployment{
    45  		ObjectMeta: metav1.ObjectMeta{
    46  			Name: "progress-test",
    47  		},
    48  		Spec: apps.DeploymentSpec{
    49  			ProgressDeadlineSeconds: pds,
    50  			Replicas:                &replicas,
    51  			Strategy: apps.DeploymentStrategy{
    52  				Type: apps.RecreateDeploymentStrategyType,
    53  			},
    54  		},
    55  		Status: newDeploymentStatus(statusReplicas, updatedReplicas, availableReplicas),
    56  	}
    57  	d.Status.Conditions = conditions
    58  	return d
    59  }
    60  
    61  // helper to create RS with given availableReplicas
    62  func newRSWithAvailable(name string, specReplicas, statusReplicas, availableReplicas int32) *apps.ReplicaSet {
    63  	rs := rs(name, specReplicas, nil, metav1.Time{})
    64  	rs.Status = apps.ReplicaSetStatus{
    65  		Replicas:          statusReplicas,
    66  		AvailableReplicas: availableReplicas,
    67  	}
    68  	return rs
    69  }
    70  
    71  func TestRequeueStuckDeployment(t *testing.T) {
    72  	pds := int32(60)
    73  	infinite := int32(math.MaxInt32)
    74  	failed := []apps.DeploymentCondition{
    75  		{
    76  			Type:   apps.DeploymentProgressing,
    77  			Status: v1.ConditionFalse,
    78  			Reason: util.TimedOutReason,
    79  		},
    80  	}
    81  	stuck := []apps.DeploymentCondition{
    82  		{
    83  			Type:           apps.DeploymentProgressing,
    84  			Status:         v1.ConditionTrue,
    85  			LastUpdateTime: metav1.Date(2017, 2, 15, 18, 49, 00, 00, time.UTC),
    86  		},
    87  	}
    88  
    89  	tests := []struct {
    90  		name     string
    91  		d        *apps.Deployment
    92  		status   apps.DeploymentStatus
    93  		nowFn    func() time.Time
    94  		expected time.Duration
    95  	}{
    96  		{
    97  			name:     "nil progressDeadlineSeconds specified",
    98  			d:        currentDeployment(nil, 4, 3, 3, 2, nil),
    99  			status:   newDeploymentStatus(3, 3, 2),
   100  			expected: time.Duration(-1),
   101  		},
   102  		{
   103  			name:     "infinite progressDeadlineSeconds specified",
   104  			d:        currentDeployment(&infinite, 4, 3, 3, 2, nil),
   105  			status:   newDeploymentStatus(3, 3, 2),
   106  			expected: time.Duration(-1),
   107  		},
   108  		{
   109  			name:     "no progressing condition found",
   110  			d:        currentDeployment(&pds, 4, 3, 3, 2, nil),
   111  			status:   newDeploymentStatus(3, 3, 2),
   112  			expected: time.Duration(-1),
   113  		},
   114  		{
   115  			name:     "complete deployment does not need to be requeued",
   116  			d:        currentDeployment(&pds, 3, 3, 3, 3, nil),
   117  			status:   newDeploymentStatus(3, 3, 3),
   118  			expected: time.Duration(-1),
   119  		},
   120  		{
   121  			name:     "already failed deployment does not need to be requeued",
   122  			d:        currentDeployment(&pds, 3, 3, 3, 0, failed),
   123  			status:   newDeploymentStatus(3, 3, 0),
   124  			expected: time.Duration(-1),
   125  		},
   126  		{
   127  			name:     "stuck deployment - 30s",
   128  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   129  			status:   newDeploymentStatus(3, 3, 1),
   130  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 49, 30, 00, time.UTC).Time },
   131  			expected: 30 * time.Second,
   132  		},
   133  		{
   134  			name:     "stuck deployment - 1s",
   135  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   136  			status:   newDeploymentStatus(3, 3, 1),
   137  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 49, 59, 00, time.UTC).Time },
   138  			expected: 1 * time.Second,
   139  		},
   140  		{
   141  			name:     "failed deployment - less than a second => now",
   142  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   143  			status:   newDeploymentStatus(3, 3, 1),
   144  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 49, 59, 1, time.UTC).Time },
   145  			expected: time.Duration(0),
   146  		},
   147  		{
   148  			name:     "failed deployment - now",
   149  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   150  			status:   newDeploymentStatus(3, 3, 1),
   151  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 50, 00, 00, time.UTC).Time },
   152  			expected: time.Duration(0),
   153  		},
   154  		{
   155  			name:     "failed deployment - 1s after deadline",
   156  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   157  			status:   newDeploymentStatus(3, 3, 1),
   158  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 50, 01, 00, time.UTC).Time },
   159  			expected: time.Duration(0),
   160  		},
   161  		{
   162  			name:     "failed deployment - 60s after deadline",
   163  			d:        currentDeployment(&pds, 3, 3, 3, 1, stuck),
   164  			status:   newDeploymentStatus(3, 3, 1),
   165  			nowFn:    func() time.Time { return metav1.Date(2017, 2, 15, 18, 51, 00, 00, time.UTC).Time },
   166  			expected: time.Duration(0),
   167  		},
   168  	}
   169  
   170  	dc := &DeploymentController{
   171  		queue: workqueue.NewTypedRateLimitingQueueWithConfig(
   172  			workqueue.DefaultTypedControllerRateLimiter[string](),
   173  			workqueue.TypedRateLimitingQueueConfig[string]{
   174  				Name: "doesnt_matter",
   175  			},
   176  		),
   177  	}
   178  	dc.enqueueDeployment = dc.enqueue
   179  
   180  	for _, test := range tests {
   181  		t.Run(test.name, func(t *testing.T) {
   182  			if test.nowFn != nil {
   183  				nowFn = test.nowFn
   184  			}
   185  			_, ctx := ktesting.NewTestContext(t)
   186  			ctx, cancel := context.WithCancel(ctx)
   187  			defer cancel()
   188  			got := dc.requeueStuckDeployment(ctx, test.d, test.status)
   189  			if got != test.expected {
   190  				t.Errorf("%s: got duration: %v, expected duration: %v", test.name, got, test.expected)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func TestSyncRolloutStatus(t *testing.T) {
   197  	pds := int32(60)
   198  	testTime := metav1.Date(2017, 2, 15, 18, 49, 00, 00, time.UTC)
   199  	failedTimedOut := apps.DeploymentCondition{
   200  		Type:   apps.DeploymentProgressing,
   201  		Status: v1.ConditionFalse,
   202  		Reason: util.TimedOutReason,
   203  	}
   204  	newRSAvailable := apps.DeploymentCondition{
   205  		Type:               apps.DeploymentProgressing,
   206  		Status:             v1.ConditionTrue,
   207  		Reason:             util.NewRSAvailableReason,
   208  		LastUpdateTime:     testTime,
   209  		LastTransitionTime: testTime,
   210  	}
   211  	replicaSetUpdated := apps.DeploymentCondition{
   212  		Type:               apps.DeploymentProgressing,
   213  		Status:             v1.ConditionTrue,
   214  		Reason:             util.ReplicaSetUpdatedReason,
   215  		LastUpdateTime:     testTime,
   216  		LastTransitionTime: testTime,
   217  	}
   218  
   219  	tests := []struct {
   220  		name            string
   221  		d               *apps.Deployment
   222  		allRSs          []*apps.ReplicaSet
   223  		newRS           *apps.ReplicaSet
   224  		conditionType   apps.DeploymentConditionType
   225  		conditionStatus v1.ConditionStatus
   226  		conditionReason string
   227  		lastUpdate      metav1.Time
   228  		lastTransition  metav1.Time
   229  	}{
   230  		{
   231  			name:   "General: remove Progressing condition and do not estimate progress if deployment has no Progress Deadline",
   232  			d:      currentDeployment(nil, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}),
   233  			allRSs: []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   234  			newRS:  newRSWithAvailable("foo", 3, 2, 2),
   235  		},
   236  		{
   237  			name:            "General: do not estimate progress of deployment with only one active ReplicaSet",
   238  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{newRSAvailable}),
   239  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 3, 3, 3)},
   240  			conditionType:   apps.DeploymentProgressing,
   241  			conditionStatus: v1.ConditionTrue,
   242  			conditionReason: util.NewRSAvailableReason,
   243  			lastUpdate:      testTime,
   244  			lastTransition:  testTime,
   245  		},
   246  		{
   247  			name:            "DeploymentProgressing: dont update lastTransitionTime if deployment already has Progressing=True",
   248  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}),
   249  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   250  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   251  			conditionType:   apps.DeploymentProgressing,
   252  			conditionStatus: v1.ConditionTrue,
   253  			conditionReason: util.ReplicaSetUpdatedReason,
   254  			lastTransition:  testTime,
   255  		},
   256  		{
   257  			name:            "DeploymentProgressing: update everything if deployment has Progressing=False",
   258  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{failedTimedOut}),
   259  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   260  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   261  			conditionType:   apps.DeploymentProgressing,
   262  			conditionStatus: v1.ConditionTrue,
   263  			conditionReason: util.ReplicaSetUpdatedReason,
   264  		},
   265  		{
   266  			name:            "DeploymentProgressing: create Progressing condition if it does not exist",
   267  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{}),
   268  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)},
   269  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   270  			conditionType:   apps.DeploymentProgressing,
   271  			conditionStatus: v1.ConditionTrue,
   272  			conditionReason: util.ReplicaSetUpdatedReason,
   273  		},
   274  		{
   275  			name:            "DeploymentComplete: dont update lastTransitionTime if deployment already has Progressing=True",
   276  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{replicaSetUpdated}),
   277  			allRSs:          []*apps.ReplicaSet{},
   278  			newRS:           newRSWithAvailable("foo", 3, 3, 3),
   279  			conditionType:   apps.DeploymentProgressing,
   280  			conditionStatus: v1.ConditionTrue,
   281  			conditionReason: util.NewRSAvailableReason,
   282  			lastTransition:  testTime,
   283  		},
   284  		{
   285  			name:            "DeploymentComplete: update everything if deployment has Progressing=False",
   286  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{failedTimedOut}),
   287  			allRSs:          []*apps.ReplicaSet{},
   288  			newRS:           newRSWithAvailable("foo", 3, 3, 3),
   289  			conditionType:   apps.DeploymentProgressing,
   290  			conditionStatus: v1.ConditionTrue,
   291  			conditionReason: util.NewRSAvailableReason,
   292  		},
   293  		{
   294  			name:            "DeploymentComplete: create Progressing condition if it does not exist",
   295  			d:               currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{}),
   296  			allRSs:          []*apps.ReplicaSet{},
   297  			newRS:           newRSWithAvailable("foo", 3, 3, 3),
   298  			conditionType:   apps.DeploymentProgressing,
   299  			conditionStatus: v1.ConditionTrue,
   300  			conditionReason: util.NewRSAvailableReason,
   301  		},
   302  		{
   303  			name:            "DeploymentComplete: defend against NPE when newRS=nil",
   304  			d:               currentDeployment(&pds, 0, 3, 3, 3, []apps.DeploymentCondition{replicaSetUpdated}),
   305  			allRSs:          []*apps.ReplicaSet{newRSWithAvailable("foo", 0, 0, 0)},
   306  			conditionType:   apps.DeploymentProgressing,
   307  			conditionStatus: v1.ConditionTrue,
   308  			conditionReason: util.NewRSAvailableReason,
   309  		},
   310  		{
   311  			name:            "DeploymentTimedOut: update status if rollout exceeds Progress Deadline",
   312  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}),
   313  			allRSs:          []*apps.ReplicaSet{},
   314  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   315  			conditionType:   apps.DeploymentProgressing,
   316  			conditionStatus: v1.ConditionFalse,
   317  			conditionReason: util.TimedOutReason,
   318  		},
   319  		{
   320  			name:            "DeploymentTimedOut: do not update status if deployment has existing timedOut condition",
   321  			d:               currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{failedTimedOut}),
   322  			allRSs:          []*apps.ReplicaSet{},
   323  			newRS:           newRSWithAvailable("foo", 3, 2, 2),
   324  			conditionType:   apps.DeploymentProgressing,
   325  			conditionStatus: v1.ConditionFalse,
   326  			conditionReason: util.TimedOutReason,
   327  			lastUpdate:      testTime,
   328  			lastTransition:  testTime,
   329  		},
   330  	}
   331  
   332  	for _, test := range tests {
   333  		t.Run(test.name, func(t *testing.T) {
   334  			fake := fake.Clientset{}
   335  			dc := &DeploymentController{
   336  				client: &fake,
   337  			}
   338  
   339  			if test.newRS != nil {
   340  				test.allRSs = append(test.allRSs, test.newRS)
   341  			}
   342  			_, ctx := ktesting.NewTestContext(t)
   343  			err := dc.syncRolloutStatus(ctx, test.allRSs, test.newRS, test.d)
   344  			if err != nil {
   345  				t.Error(err)
   346  			}
   347  
   348  			newCond := util.GetDeploymentCondition(test.d.Status, test.conditionType)
   349  			switch {
   350  			case newCond == nil:
   351  				if test.d.Spec.ProgressDeadlineSeconds != nil && *test.d.Spec.ProgressDeadlineSeconds != math.MaxInt32 {
   352  					t.Errorf("%s: expected deployment condition: %s", test.name, test.conditionType)
   353  				}
   354  			case newCond.Status != test.conditionStatus || newCond.Reason != test.conditionReason:
   355  				t.Errorf("%s: DeploymentProgressing has status %s with reason %s. Expected %s with %s.", test.name, newCond.Status, newCond.Reason, test.conditionStatus, test.conditionReason)
   356  			case !test.lastUpdate.IsZero() && test.lastUpdate != testTime:
   357  				t.Errorf("%s: LastUpdateTime was changed to %s but expected %s;", test.name, test.lastUpdate, testTime)
   358  			case !test.lastTransition.IsZero() && test.lastTransition != testTime:
   359  				t.Errorf("%s: LastTransitionTime was changed to %s but expected %s;", test.name, test.lastTransition, testTime)
   360  			}
   361  		})
   362  	}
   363  }