k8s.io/kubernetes@v1.29.3/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.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "doesnt_matter"), 172 } 173 dc.enqueueDeployment = dc.enqueue 174 175 for _, test := range tests { 176 t.Run(test.name, func(t *testing.T) { 177 if test.nowFn != nil { 178 nowFn = test.nowFn 179 } 180 _, ctx := ktesting.NewTestContext(t) 181 ctx, cancel := context.WithCancel(ctx) 182 defer cancel() 183 got := dc.requeueStuckDeployment(ctx, test.d, test.status) 184 if got != test.expected { 185 t.Errorf("%s: got duration: %v, expected duration: %v", test.name, got, test.expected) 186 } 187 }) 188 } 189 } 190 191 func TestSyncRolloutStatus(t *testing.T) { 192 pds := int32(60) 193 testTime := metav1.Date(2017, 2, 15, 18, 49, 00, 00, time.UTC) 194 failedTimedOut := apps.DeploymentCondition{ 195 Type: apps.DeploymentProgressing, 196 Status: v1.ConditionFalse, 197 Reason: util.TimedOutReason, 198 } 199 newRSAvailable := apps.DeploymentCondition{ 200 Type: apps.DeploymentProgressing, 201 Status: v1.ConditionTrue, 202 Reason: util.NewRSAvailableReason, 203 LastUpdateTime: testTime, 204 LastTransitionTime: testTime, 205 } 206 replicaSetUpdated := apps.DeploymentCondition{ 207 Type: apps.DeploymentProgressing, 208 Status: v1.ConditionTrue, 209 Reason: util.ReplicaSetUpdatedReason, 210 LastUpdateTime: testTime, 211 LastTransitionTime: testTime, 212 } 213 214 tests := []struct { 215 name string 216 d *apps.Deployment 217 allRSs []*apps.ReplicaSet 218 newRS *apps.ReplicaSet 219 conditionType apps.DeploymentConditionType 220 conditionStatus v1.ConditionStatus 221 conditionReason string 222 lastUpdate metav1.Time 223 lastTransition metav1.Time 224 }{ 225 { 226 name: "General: remove Progressing condition and do not estimate progress if deployment has no Progress Deadline", 227 d: currentDeployment(nil, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}), 228 allRSs: []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)}, 229 newRS: newRSWithAvailable("foo", 3, 2, 2), 230 }, 231 { 232 name: "General: do not estimate progress of deployment with only one active ReplicaSet", 233 d: currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{newRSAvailable}), 234 allRSs: []*apps.ReplicaSet{newRSWithAvailable("bar", 3, 3, 3)}, 235 conditionType: apps.DeploymentProgressing, 236 conditionStatus: v1.ConditionTrue, 237 conditionReason: util.NewRSAvailableReason, 238 lastUpdate: testTime, 239 lastTransition: testTime, 240 }, 241 { 242 name: "DeploymentProgressing: dont update lastTransitionTime if deployment already has Progressing=True", 243 d: currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}), 244 allRSs: []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)}, 245 newRS: newRSWithAvailable("foo", 3, 2, 2), 246 conditionType: apps.DeploymentProgressing, 247 conditionStatus: v1.ConditionTrue, 248 conditionReason: util.ReplicaSetUpdatedReason, 249 lastTransition: testTime, 250 }, 251 { 252 name: "DeploymentProgressing: update everything if deployment has Progressing=False", 253 d: currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{failedTimedOut}), 254 allRSs: []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)}, 255 newRS: newRSWithAvailable("foo", 3, 2, 2), 256 conditionType: apps.DeploymentProgressing, 257 conditionStatus: v1.ConditionTrue, 258 conditionReason: util.ReplicaSetUpdatedReason, 259 }, 260 { 261 name: "DeploymentProgressing: create Progressing condition if it does not exist", 262 d: currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{}), 263 allRSs: []*apps.ReplicaSet{newRSWithAvailable("bar", 0, 1, 1)}, 264 newRS: newRSWithAvailable("foo", 3, 2, 2), 265 conditionType: apps.DeploymentProgressing, 266 conditionStatus: v1.ConditionTrue, 267 conditionReason: util.ReplicaSetUpdatedReason, 268 }, 269 { 270 name: "DeploymentComplete: dont update lastTransitionTime if deployment already has Progressing=True", 271 d: currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{replicaSetUpdated}), 272 allRSs: []*apps.ReplicaSet{}, 273 newRS: newRSWithAvailable("foo", 3, 3, 3), 274 conditionType: apps.DeploymentProgressing, 275 conditionStatus: v1.ConditionTrue, 276 conditionReason: util.NewRSAvailableReason, 277 lastTransition: testTime, 278 }, 279 { 280 name: "DeploymentComplete: update everything if deployment has Progressing=False", 281 d: currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{failedTimedOut}), 282 allRSs: []*apps.ReplicaSet{}, 283 newRS: newRSWithAvailable("foo", 3, 3, 3), 284 conditionType: apps.DeploymentProgressing, 285 conditionStatus: v1.ConditionTrue, 286 conditionReason: util.NewRSAvailableReason, 287 }, 288 { 289 name: "DeploymentComplete: create Progressing condition if it does not exist", 290 d: currentDeployment(&pds, 3, 3, 3, 3, []apps.DeploymentCondition{}), 291 allRSs: []*apps.ReplicaSet{}, 292 newRS: newRSWithAvailable("foo", 3, 3, 3), 293 conditionType: apps.DeploymentProgressing, 294 conditionStatus: v1.ConditionTrue, 295 conditionReason: util.NewRSAvailableReason, 296 }, 297 { 298 name: "DeploymentComplete: defend against NPE when newRS=nil", 299 d: currentDeployment(&pds, 0, 3, 3, 3, []apps.DeploymentCondition{replicaSetUpdated}), 300 allRSs: []*apps.ReplicaSet{newRSWithAvailable("foo", 0, 0, 0)}, 301 conditionType: apps.DeploymentProgressing, 302 conditionStatus: v1.ConditionTrue, 303 conditionReason: util.NewRSAvailableReason, 304 }, 305 { 306 name: "DeploymentTimedOut: update status if rollout exceeds Progress Deadline", 307 d: currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{replicaSetUpdated}), 308 allRSs: []*apps.ReplicaSet{}, 309 newRS: newRSWithAvailable("foo", 3, 2, 2), 310 conditionType: apps.DeploymentProgressing, 311 conditionStatus: v1.ConditionFalse, 312 conditionReason: util.TimedOutReason, 313 }, 314 { 315 name: "DeploymentTimedOut: do not update status if deployment has existing timedOut condition", 316 d: currentDeployment(&pds, 3, 2, 2, 2, []apps.DeploymentCondition{failedTimedOut}), 317 allRSs: []*apps.ReplicaSet{}, 318 newRS: newRSWithAvailable("foo", 3, 2, 2), 319 conditionType: apps.DeploymentProgressing, 320 conditionStatus: v1.ConditionFalse, 321 conditionReason: util.TimedOutReason, 322 lastUpdate: testTime, 323 lastTransition: testTime, 324 }, 325 } 326 327 for _, test := range tests { 328 t.Run(test.name, func(t *testing.T) { 329 fake := fake.Clientset{} 330 dc := &DeploymentController{ 331 client: &fake, 332 } 333 334 if test.newRS != nil { 335 test.allRSs = append(test.allRSs, test.newRS) 336 } 337 _, ctx := ktesting.NewTestContext(t) 338 err := dc.syncRolloutStatus(ctx, test.allRSs, test.newRS, test.d) 339 if err != nil { 340 t.Error(err) 341 } 342 343 newCond := util.GetDeploymentCondition(test.d.Status, test.conditionType) 344 switch { 345 case newCond == nil: 346 if test.d.Spec.ProgressDeadlineSeconds != nil && *test.d.Spec.ProgressDeadlineSeconds != math.MaxInt32 { 347 t.Errorf("%s: expected deployment condition: %s", test.name, test.conditionType) 348 } 349 case newCond.Status != test.conditionStatus || newCond.Reason != test.conditionReason: 350 t.Errorf("%s: DeploymentProgressing has status %s with reason %s. Expected %s with %s.", test.name, newCond.Status, newCond.Reason, test.conditionStatus, test.conditionReason) 351 case !test.lastUpdate.IsZero() && test.lastUpdate != testTime: 352 t.Errorf("%s: LastUpdateTime was changed to %s but expected %s;", test.name, test.lastUpdate, testTime) 353 case !test.lastTransition.IsZero() && test.lastTransition != testTime: 354 t.Errorf("%s: LastTransitionTime was changed to %s but expected %s;", test.name, test.lastTransition, testTime) 355 } 356 }) 357 } 358 }