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 }