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