1/* 2Copyright 2016 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package deployment 18 19import ( 20 "context" 21 "sort" 22 "strconv" 23 24 appsv1 "k8s.io/api/apps/v1" 25 corev1 "k8s.io/api/core/v1" 26 apiequality "k8s.io/apimachinery/pkg/api/equality" 27 "k8s.io/apimachinery/pkg/api/meta" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/runtime" 30 intstrutil "k8s.io/apimachinery/pkg/util/intstr" 31 appsclient "k8s.io/client-go/kubernetes/typed/apps/v1" 32) 33 34const ( 35 // RevisionAnnotation is the revision annotation of a deployment's replica sets which records its rollout sequence 36 RevisionAnnotation = "deployment.kubernetes.io/revision" 37 // RevisionHistoryAnnotation maintains the history of all old revisions that a replica set has served for a deployment. 38 RevisionHistoryAnnotation = "deployment.kubernetes.io/revision-history" 39 // DesiredReplicasAnnotation is the desired replicas for a deployment recorded as an annotation 40 // in its replica sets. Helps in separating scaling events from the rollout process and for 41 // determining if the new replica set for a deployment is really saturated. 42 DesiredReplicasAnnotation = "deployment.kubernetes.io/desired-replicas" 43 // MaxReplicasAnnotation is the maximum replicas a deployment can have at a given point, which 44 // is deployment.spec.replicas + maxSurge. Used by the underlying replica sets to estimate their 45 // proportions in case the deployment has surge replicas. 46 MaxReplicasAnnotation = "deployment.kubernetes.io/max-replicas" 47 // RollbackRevisionNotFound is not found rollback event reason 48 RollbackRevisionNotFound = "DeploymentRollbackRevisionNotFound" 49 // RollbackTemplateUnchanged is the template unchanged rollback event reason 50 RollbackTemplateUnchanged = "DeploymentRollbackTemplateUnchanged" 51 // RollbackDone is the done rollback event reason 52 RollbackDone = "DeploymentRollback" 53 // TimedOutReason is added in a deployment when its newest replica set fails to show any progress 54 // within the given deadline (progressDeadlineSeconds). 55 TimedOutReason = "ProgressDeadlineExceeded" 56) 57 58// GetDeploymentCondition returns the condition with the provided type. 59func GetDeploymentCondition(status appsv1.DeploymentStatus, condType appsv1.DeploymentConditionType) *appsv1.DeploymentCondition { 60 for i := range status.Conditions { 61 c := status.Conditions[i] 62 if c.Type == condType { 63 return &c 64 } 65 } 66 return nil 67} 68 69// Revision returns the revision number of the input object. 70func Revision(obj runtime.Object) (int64, error) { 71 acc, err := meta.Accessor(obj) 72 if err != nil { 73 return 0, err 74 } 75 v, ok := acc.GetAnnotations()[RevisionAnnotation] 76 if !ok { 77 return 0, nil 78 } 79 return strconv.ParseInt(v, 10, 64) 80} 81 82// GetAllReplicaSets returns the old and new replica sets targeted by the given Deployment. It gets PodList and 83// ReplicaSetList from client interface. Note that the first set of old replica sets doesn't include the ones 84// with no pods, and the second set of old replica sets include all old replica sets. The third returned value 85// is the new replica set, and it may be nil if it doesn't exist yet. 86func GetAllReplicaSets(deployment *appsv1.Deployment, c appsclient.AppsV1Interface) ([]*appsv1.ReplicaSet, []*appsv1.ReplicaSet, *appsv1.ReplicaSet, error) { 87 rsList, err := listReplicaSets(deployment, rsListFromClient(c)) 88 if err != nil { 89 return nil, nil, nil, err 90 } 91 newRS := findNewReplicaSet(deployment, rsList) 92 oldRSes, allOldRSes := findOldReplicaSets(deployment, rsList, newRS) 93 return oldRSes, allOldRSes, newRS, nil 94} 95 96// RsListFromClient returns an rsListFunc that wraps the given client. 97func rsListFromClient(c appsclient.AppsV1Interface) rsListFunc { 98 return func(namespace string, options metav1.ListOptions) ([]*appsv1.ReplicaSet, error) { 99 rsList, err := c.ReplicaSets(namespace).List(context.TODO(), options) 100 if err != nil { 101 return nil, err 102 } 103 var ret []*appsv1.ReplicaSet 104 for i := range rsList.Items { 105 ret = append(ret, &rsList.Items[i]) 106 } 107 return ret, err 108 } 109} 110 111// TODO: switch this to full namespacers 112type rsListFunc func(string, metav1.ListOptions) ([]*appsv1.ReplicaSet, error) 113 114// listReplicaSets returns a slice of RSes the given deployment targets. 115// Note that this does NOT attempt to reconcile ControllerRef (adopt/orphan), 116// because only the controller itself should do that. 117// However, it does filter out anything whose ControllerRef doesn't match. 118func listReplicaSets(deployment *appsv1.Deployment, getRSList rsListFunc) ([]*appsv1.ReplicaSet, error) { 119 // TODO: Right now we list replica sets by their labels. We should list them by selector, i.e. the replica set's selector 120 // should be a superset of the deployment's selector, see https://github.com/kubernetes/kubernetes/issues/19830. 121 namespace := deployment.Namespace 122 selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector) 123 if err != nil { 124 return nil, err 125 } 126 options := metav1.ListOptions{LabelSelector: selector.String()} 127 all, err := getRSList(namespace, options) 128 if err != nil { 129 return nil, err 130 } 131 // Only include those whose ControllerRef matches the Deployment. 132 owned := make([]*appsv1.ReplicaSet, 0, len(all)) 133 for _, rs := range all { 134 if metav1.IsControlledBy(rs, deployment) { 135 owned = append(owned, rs) 136 } 137 } 138 return owned, nil 139} 140 141// EqualIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash] 142// We ignore pod-template-hash because: 143// 1. The hash result would be different upon podTemplateSpec API changes 144// (e.g. the addition of a new field will cause the hash code to change) 145// 2. The deployment template won't have hash labels 146func equalIgnoreHash(template1, template2 *corev1.PodTemplateSpec) bool { 147 t1Copy := template1.DeepCopy() 148 t2Copy := template2.DeepCopy() 149 // Remove hash labels from template.Labels before comparing 150 delete(t1Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey) 151 delete(t2Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey) 152 return apiequality.Semantic.DeepEqual(t1Copy, t2Copy) 153} 154 155// FindNewReplicaSet returns the new RS this given deployment targets (the one with the same pod template). 156func findNewReplicaSet(deployment *appsv1.Deployment, rsList []*appsv1.ReplicaSet) *appsv1.ReplicaSet { 157 sort.Sort(replicaSetsByCreationTimestamp(rsList)) 158 for i := range rsList { 159 if equalIgnoreHash(&rsList[i].Spec.Template, &deployment.Spec.Template) { 160 // In rare cases, such as after cluster upgrades, Deployment may end up with 161 // having more than one new ReplicaSets that have the same template as its template, 162 // see https://github.com/kubernetes/kubernetes/issues/40415 163 // We deterministically choose the oldest new ReplicaSet. 164 return rsList[i] 165 } 166 } 167 // new ReplicaSet does not exist. 168 return nil 169} 170 171// replicaSetsByCreationTimestamp sorts a list of ReplicaSet by creation timestamp, using their names as a tie breaker. 172type replicaSetsByCreationTimestamp []*appsv1.ReplicaSet 173 174func (o replicaSetsByCreationTimestamp) Len() int { return len(o) } 175func (o replicaSetsByCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 176func (o replicaSetsByCreationTimestamp) Less(i, j int) bool { 177 if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) { 178 return o[i].Name < o[j].Name 179 } 180 return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp) 181} 182 183// // FindOldReplicaSets returns the old replica sets targeted by the given Deployment, with the given slice of RSes. 184// // 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. 185func findOldReplicaSets(deployment *appsv1.Deployment, rsList []*appsv1.ReplicaSet, newRS *appsv1.ReplicaSet) ([]*appsv1.ReplicaSet, []*appsv1.ReplicaSet) { 186 var requiredRSs []*appsv1.ReplicaSet 187 var allRSs []*appsv1.ReplicaSet 188 for _, rs := range rsList { 189 // Filter out new replica set 190 if newRS != nil && rs.UID == newRS.UID { 191 continue 192 } 193 allRSs = append(allRSs, rs) 194 if *(rs.Spec.Replicas) != 0 { 195 requiredRSs = append(requiredRSs, rs) 196 } 197 } 198 return requiredRSs, allRSs 199} 200 201// ResolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one 202// step. For example: 203// 204// 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1) 205// 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1) 206// 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1) 207// 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1) 208// 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1) 209// 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1) 210func ResolveFenceposts(maxSurge, maxUnavailable *intstrutil.IntOrString, desired int32) (int32, int32, error) { 211 surge, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxSurge, intstrutil.FromInt(0)), int(desired), true) 212 if err != nil { 213 return 0, 0, err 214 } 215 unavailable, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxUnavailable, intstrutil.FromInt(0)), int(desired), false) 216 if err != nil { 217 return 0, 0, err 218 } 219 220 if surge == 0 && unavailable == 0 { 221 // Validation should never allow the user to explicitly use zero values for both maxSurge 222 // maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero. 223 // If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the 224 // theory that surge might not work due to quota. 225 unavailable = 1 226 } 227 228 return int32(surge), int32(unavailable), nil 229} 230