...

Source file src/k8s.io/kubectl/pkg/util/deployment/deployment.go

Documentation: k8s.io/kubectl/pkg/util/deployment

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

View as plain text