k8s.io/kubernetes@v1.29.3/test/utils/deployment.go (about) 1 /* 2 Copyright 2016 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 utils 18 19 import ( 20 "context" 21 "fmt" 22 "time" 23 24 apps "k8s.io/api/apps/v1" 25 v1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/dump" 28 "k8s.io/apimachinery/pkg/util/wait" 29 clientset "k8s.io/client-go/kubernetes" 30 podutil "k8s.io/kubernetes/pkg/api/v1/pod" 31 deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" 32 labelsutil "k8s.io/kubernetes/pkg/util/labels" 33 ) 34 35 type LogfFn func(format string, args ...interface{}) 36 37 func LogReplicaSetsOfDeployment(deployment *apps.Deployment, allOldRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet, logf LogfFn) { 38 if newRS != nil { 39 logf("New ReplicaSet %q of Deployment %q:\n%s", newRS.Name, deployment.Name, dump.Pretty(*newRS)) 40 } else { 41 logf("New ReplicaSet of Deployment %q is nil.", deployment.Name) 42 } 43 if len(allOldRSs) > 0 { 44 logf("All old ReplicaSets of Deployment %q:", deployment.Name) 45 } 46 for i := range allOldRSs { 47 logf(dump.Pretty(*allOldRSs[i])) 48 } 49 } 50 51 func LogPodsOfDeployment(c clientset.Interface, deployment *apps.Deployment, rsList []*apps.ReplicaSet, logf LogfFn) { 52 minReadySeconds := deployment.Spec.MinReadySeconds 53 podListFunc := func(namespace string, options metav1.ListOptions) (*v1.PodList, error) { 54 return c.CoreV1().Pods(namespace).List(context.TODO(), options) 55 } 56 57 podList, err := deploymentutil.ListPods(deployment, rsList, podListFunc) 58 if err != nil { 59 logf("Failed to list Pods of Deployment %q: %v", deployment.Name, err) 60 return 61 } 62 for _, pod := range podList.Items { 63 availability := "not available" 64 if podutil.IsPodAvailable(&pod, minReadySeconds, metav1.Now()) { 65 availability = "available" 66 } 67 logf("Pod %q is %s:\n%s", pod.Name, availability, dump.Pretty(pod)) 68 } 69 } 70 71 // Waits for the deployment to complete. 72 // If during a rolling update (rolling == true), returns an error if the deployment's 73 // rolling update strategy (max unavailable or max surge) is broken at any times. 74 // It's not seen as a rolling update if shortly after a scaling event or the deployment is just created. 75 func waitForDeploymentCompleteMaybeCheckRolling(c clientset.Interface, d *apps.Deployment, rolling bool, logf LogfFn, pollInterval, pollTimeout time.Duration) error { 76 var ( 77 deployment *apps.Deployment 78 reason string 79 ) 80 81 err := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) { 82 var err error 83 deployment, err = c.AppsV1().Deployments(d.Namespace).Get(context.TODO(), d.Name, metav1.GetOptions{}) 84 if err != nil { 85 return false, err 86 } 87 88 // If during a rolling update, make sure rolling update strategy isn't broken at any times. 89 if rolling { 90 reason, err = checkRollingUpdateStatus(c, deployment, logf) 91 if err != nil { 92 return false, err 93 } 94 logf(reason) 95 } 96 97 // When the deployment status and its underlying resources reach the desired state, we're done 98 if deploymentutil.DeploymentComplete(d, &deployment.Status) { 99 return true, nil 100 } 101 102 reason = fmt.Sprintf("deployment status: %#v", deployment.Status) 103 logf(reason) 104 105 return false, nil 106 }) 107 108 if err == wait.ErrWaitTimeout { 109 err = fmt.Errorf("%s", reason) 110 } 111 if err != nil { 112 return fmt.Errorf("error waiting for deployment %q status to match expectation: %v", d.Name, err) 113 } 114 return nil 115 } 116 117 func checkRollingUpdateStatus(c clientset.Interface, deployment *apps.Deployment, logf LogfFn) (string, error) { 118 var reason string 119 oldRSs, allOldRSs, newRS, err := GetAllReplicaSets(deployment, c) 120 if err != nil { 121 return "", err 122 } 123 if newRS == nil { 124 // New RC hasn't been created yet. 125 reason = "new replica set hasn't been created yet" 126 return reason, nil 127 } 128 allRSs := append(oldRSs, newRS) 129 // The old/new ReplicaSets need to contain the pod-template-hash label 130 for i := range allRSs { 131 if !labelsutil.SelectorHasLabel(allRSs[i].Spec.Selector, apps.DefaultDeploymentUniqueLabelKey) { 132 reason = "all replica sets need to contain the pod-template-hash label" 133 return reason, nil 134 } 135 } 136 137 // Check max surge and min available 138 totalCreated := deploymentutil.GetReplicaCountForReplicaSets(allRSs) 139 maxCreated := *(deployment.Spec.Replicas) + deploymentutil.MaxSurge(*deployment) 140 if totalCreated > maxCreated { 141 LogReplicaSetsOfDeployment(deployment, allOldRSs, newRS, logf) 142 LogPodsOfDeployment(c, deployment, allRSs, logf) 143 return "", fmt.Errorf("total pods created: %d, more than the max allowed: %d", totalCreated, maxCreated) 144 } 145 minAvailable := deploymentutil.MinAvailable(deployment) 146 if deployment.Status.AvailableReplicas < minAvailable { 147 LogReplicaSetsOfDeployment(deployment, allOldRSs, newRS, logf) 148 LogPodsOfDeployment(c, deployment, allRSs, logf) 149 return "", fmt.Errorf("total pods available: %d, less than the min required: %d", deployment.Status.AvailableReplicas, minAvailable) 150 } 151 return "", nil 152 } 153 154 // GetAllReplicaSets returns the old and new replica sets targeted by the given Deployment. It gets PodList and ReplicaSetList from client interface. 155 // Note that the first set of old replica sets doesn't include the ones with no pods, and the second set of old replica sets include all old replica sets. 156 // The third returned value is the new replica set, and it may be nil if it doesn't exist yet. 157 func GetAllReplicaSets(deployment *apps.Deployment, c clientset.Interface) ([]*apps.ReplicaSet, []*apps.ReplicaSet, *apps.ReplicaSet, error) { 158 rsList, err := deploymentutil.ListReplicaSets(deployment, deploymentutil.RsListFromClient(c.AppsV1())) 159 if err != nil { 160 return nil, nil, nil, err 161 } 162 oldRSes, allOldRSes := deploymentutil.FindOldReplicaSets(deployment, rsList) 163 newRS := deploymentutil.FindNewReplicaSet(deployment, rsList) 164 return oldRSes, allOldRSes, newRS, nil 165 } 166 167 // GetOldReplicaSets returns the old replica sets targeted by the given Deployment; get PodList and ReplicaSetList from client interface. 168 // Note that the first set of old replica sets doesn't include the ones with no pods, and the second set of old replica sets include all old replica sets. 169 func GetOldReplicaSets(deployment *apps.Deployment, c clientset.Interface) ([]*apps.ReplicaSet, []*apps.ReplicaSet, error) { 170 rsList, err := deploymentutil.ListReplicaSets(deployment, deploymentutil.RsListFromClient(c.AppsV1())) 171 if err != nil { 172 return nil, nil, err 173 } 174 oldRSes, allOldRSes := deploymentutil.FindOldReplicaSets(deployment, rsList) 175 return oldRSes, allOldRSes, nil 176 } 177 178 // GetNewReplicaSet returns a replica set that matches the intent of the given deployment; get ReplicaSetList from client interface. 179 // Returns nil if the new replica set doesn't exist yet. 180 func GetNewReplicaSet(deployment *apps.Deployment, c clientset.Interface) (*apps.ReplicaSet, error) { 181 rsList, err := deploymentutil.ListReplicaSets(deployment, deploymentutil.RsListFromClient(c.AppsV1())) 182 if err != nil { 183 return nil, err 184 } 185 return deploymentutil.FindNewReplicaSet(deployment, rsList), nil 186 } 187 188 // Waits for the deployment to complete, and check rolling update strategy isn't broken at any times. 189 // Rolling update strategy should not be broken during a rolling update. 190 func WaitForDeploymentCompleteAndCheckRolling(c clientset.Interface, d *apps.Deployment, logf LogfFn, pollInterval, pollTimeout time.Duration) error { 191 rolling := true 192 return waitForDeploymentCompleteMaybeCheckRolling(c, d, rolling, logf, pollInterval, pollTimeout) 193 } 194 195 // Waits for the deployment to complete, and don't check if rolling update strategy is broken. 196 // Rolling update strategy is used only during a rolling update, and can be violated in other situations, 197 // such as shortly after a scaling event or the deployment is just created. 198 func WaitForDeploymentComplete(c clientset.Interface, d *apps.Deployment, logf LogfFn, pollInterval, pollTimeout time.Duration) error { 199 rolling := false 200 return waitForDeploymentCompleteMaybeCheckRolling(c, d, rolling, logf, pollInterval, pollTimeout) 201 } 202 203 // WaitForDeploymentRevisionAndImage waits for the deployment's and its new RS's revision and container image to match the given revision and image. 204 // Note that deployment revision and its new RS revision should be updated shortly, so we only wait for 1 minute here to fail early. 205 func WaitForDeploymentRevisionAndImage(c clientset.Interface, ns, deploymentName string, revision, image string, logf LogfFn, pollInterval, pollTimeout time.Duration) error { 206 var deployment *apps.Deployment 207 var newRS *apps.ReplicaSet 208 var reason string 209 err := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) { 210 var err error 211 deployment, err = c.AppsV1().Deployments(ns).Get(context.TODO(), deploymentName, metav1.GetOptions{}) 212 if err != nil { 213 return false, err 214 } 215 // The new ReplicaSet needs to be non-nil and contain the pod-template-hash label 216 newRS, err = GetNewReplicaSet(deployment, c) 217 if err != nil { 218 return false, err 219 } 220 if err := checkRevisionAndImage(deployment, newRS, revision, image); err != nil { 221 reason = err.Error() 222 logf(reason) 223 return false, nil 224 } 225 return true, nil 226 }) 227 if err == wait.ErrWaitTimeout { 228 LogReplicaSetsOfDeployment(deployment, nil, newRS, logf) 229 err = fmt.Errorf(reason) 230 } 231 if newRS == nil { 232 return fmt.Errorf("deployment %q failed to create new replica set", deploymentName) 233 } 234 if err != nil { 235 if deployment == nil { 236 return fmt.Errorf("error creating new replica set for deployment %q: %w", deploymentName, err) 237 } 238 deploymentImage := "" 239 if len(deployment.Spec.Template.Spec.Containers) > 0 { 240 deploymentImage = deployment.Spec.Template.Spec.Containers[0].Image 241 } 242 newRSImage := "" 243 if len(newRS.Spec.Template.Spec.Containers) > 0 { 244 newRSImage = newRS.Spec.Template.Spec.Containers[0].Image 245 } 246 return fmt.Errorf("error waiting for deployment %q (got %s / %s) and new replica set %q (got %s / %s) revision and image to match expectation (expected %s / %s): %v", deploymentName, deployment.Annotations[deploymentutil.RevisionAnnotation], deploymentImage, newRS.Name, newRS.Annotations[deploymentutil.RevisionAnnotation], newRSImage, revision, image, err) 247 } 248 return nil 249 } 250 251 // CheckDeploymentRevisionAndImage checks if the input deployment's and its new replica set's revision and image are as expected. 252 func CheckDeploymentRevisionAndImage(c clientset.Interface, ns, deploymentName, revision, image string) error { 253 deployment, err := c.AppsV1().Deployments(ns).Get(context.TODO(), deploymentName, metav1.GetOptions{}) 254 if err != nil { 255 return fmt.Errorf("unable to get deployment %s during revision check: %v", deploymentName, err) 256 } 257 258 // Check revision of the new replica set of this deployment 259 newRS, err := GetNewReplicaSet(deployment, c) 260 if err != nil { 261 return fmt.Errorf("unable to get new replicaset of deployment %s during revision check: %v", deploymentName, err) 262 } 263 return checkRevisionAndImage(deployment, newRS, revision, image) 264 } 265 266 func checkRevisionAndImage(deployment *apps.Deployment, newRS *apps.ReplicaSet, revision, image string) error { 267 // The new ReplicaSet needs to be non-nil and contain the pod-template-hash label 268 if newRS == nil { 269 return fmt.Errorf("new replicaset for deployment %q is yet to be created", deployment.Name) 270 } 271 if !labelsutil.SelectorHasLabel(newRS.Spec.Selector, apps.DefaultDeploymentUniqueLabelKey) { 272 return fmt.Errorf("new replica set %q doesn't have %q label selector", newRS.Name, apps.DefaultDeploymentUniqueLabelKey) 273 } 274 // Check revision of this deployment, and of the new replica set of this deployment 275 if deployment.Annotations == nil || deployment.Annotations[deploymentutil.RevisionAnnotation] != revision { 276 return fmt.Errorf("deployment %q doesn't have the required revision set", deployment.Name) 277 } 278 if newRS.Annotations == nil || newRS.Annotations[deploymentutil.RevisionAnnotation] != revision { 279 return fmt.Errorf("new replicaset %q doesn't have the required revision set", newRS.Name) 280 } 281 // Check the image of this deployment, and of the new replica set of this deployment 282 if !containsImage(deployment.Spec.Template.Spec.Containers, image) { 283 return fmt.Errorf("deployment %q doesn't have the required image %s set", deployment.Name, image) 284 } 285 if !containsImage(newRS.Spec.Template.Spec.Containers, image) { 286 return fmt.Errorf("new replica set %q doesn't have the required image %s.", newRS.Name, image) 287 } 288 return nil 289 } 290 291 func containsImage(containers []v1.Container, imageName string) bool { 292 for _, container := range containers { 293 if container.Image == imageName { 294 return true 295 } 296 } 297 return false 298 } 299 300 type UpdateDeploymentFunc func(d *apps.Deployment) 301 302 func UpdateDeploymentWithRetries(c clientset.Interface, namespace, name string, applyUpdate UpdateDeploymentFunc, logf LogfFn, pollInterval, pollTimeout time.Duration) (*apps.Deployment, error) { 303 var deployment *apps.Deployment 304 var updateErr error 305 pollErr := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) { 306 var err error 307 if deployment, err = c.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{}); err != nil { 308 return false, err 309 } 310 // Apply the update, then attempt to push it to the apiserver. 311 applyUpdate(deployment) 312 if deployment, err = c.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, metav1.UpdateOptions{}); err == nil { 313 logf("Updating deployment %s", name) 314 return true, nil 315 } 316 updateErr = err 317 return false, nil 318 }) 319 if pollErr == wait.ErrWaitTimeout { 320 pollErr = fmt.Errorf("couldn't apply the provided updated to deployment %q: %v", name, updateErr) 321 } 322 return deployment, pollErr 323 } 324 325 func WaitForObservedDeployment(c clientset.Interface, ns, deploymentName string, desiredGeneration int64) error { 326 return deploymentutil.WaitForObservedDeployment(func() (*apps.Deployment, error) { 327 return c.AppsV1().Deployments(ns).Get(context.TODO(), deploymentName, metav1.GetOptions{}) 328 }, desiredGeneration, 2*time.Second, 1*time.Minute) 329 } 330 331 // WaitForDeploymentRollbackCleared waits for given deployment either started rolling back or doesn't need to rollback. 332 func WaitForDeploymentRollbackCleared(c clientset.Interface, ns, deploymentName string, pollInterval, pollTimeout time.Duration) error { 333 err := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) { 334 deployment, err := c.AppsV1().Deployments(ns).Get(context.TODO(), deploymentName, metav1.GetOptions{}) 335 if err != nil { 336 return false, err 337 } 338 // Rollback not set or is kicked off 339 if deployment.Annotations[apps.DeprecatedRollbackTo] == "" { 340 return true, nil 341 } 342 return false, nil 343 }) 344 if err != nil { 345 return fmt.Errorf("error waiting for deployment %s rollbackTo to be cleared: %v", deploymentName, err) 346 } 347 return nil 348 } 349 350 // WaitForDeploymentUpdatedReplicasGTE waits for given deployment to be observed by the controller and has at least a number of updatedReplicas 351 func WaitForDeploymentUpdatedReplicasGTE(c clientset.Interface, ns, deploymentName string, minUpdatedReplicas int32, desiredGeneration int64, pollInterval, pollTimeout time.Duration) error { 352 var deployment *apps.Deployment 353 err := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) { 354 d, err := c.AppsV1().Deployments(ns).Get(context.TODO(), deploymentName, metav1.GetOptions{}) 355 if err != nil { 356 return false, err 357 } 358 deployment = d 359 return deployment.Status.ObservedGeneration >= desiredGeneration && deployment.Status.UpdatedReplicas >= minUpdatedReplicas, nil 360 }) 361 if err != nil { 362 return fmt.Errorf("error waiting for deployment %q to have at least %d updatedReplicas: %v; latest .status.updatedReplicas: %d", deploymentName, minUpdatedReplicas, err, deployment.Status.UpdatedReplicas) 363 } 364 return nil 365 } 366 367 func WaitForDeploymentWithCondition(c clientset.Interface, ns, deploymentName, reason string, condType apps.DeploymentConditionType, logf LogfFn, pollInterval, pollTimeout time.Duration) error { 368 var deployment *apps.Deployment 369 pollErr := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) { 370 d, err := c.AppsV1().Deployments(ns).Get(context.TODO(), deploymentName, metav1.GetOptions{}) 371 if err != nil { 372 return false, err 373 } 374 deployment = d 375 cond := deploymentutil.GetDeploymentCondition(deployment.Status, condType) 376 return cond != nil && cond.Reason == reason, nil 377 }) 378 if pollErr == wait.ErrWaitTimeout { 379 pollErr = fmt.Errorf("deployment %q never updated with the desired condition and reason, latest deployment conditions: %+v", deployment.Name, deployment.Status.Conditions) 380 _, allOldRSs, newRS, err := GetAllReplicaSets(deployment, c) 381 if err == nil { 382 LogReplicaSetsOfDeployment(deployment, allOldRSs, newRS, logf) 383 LogPodsOfDeployment(c, deployment, append(allOldRSs, newRS), logf) 384 } 385 } 386 return pollErr 387 }