-
Notifications
You must be signed in to change notification settings - Fork 0
/
conditions.go
389 lines (347 loc) · 18.7 KB
/
conditions.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
package conditions
import (
"fmt"
"hash/fnv"
"math"
"reflect"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/kubernetes/pkg/controller"
hashutil "k8s.io/kubernetes/pkg/util/hash"
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
"github.com/argoproj/argo-rollouts/utils/defaults"
logutil "github.com/argoproj/argo-rollouts/utils/log"
)
const (
// Verify Spec constants
// InvalidSpecReason indicates that the spec is invalid
InvalidSpecReason = "InvalidSpec"
// MissingFieldMessage the message to indicate rollout is missing a field
MissingFieldMessage = "Rollout has missing field '%s'"
// SelectAllMessage the message to indicate that the rollout has an empty selector
SelectAllMessage = "This rollout is selecting all pods. A non-empty selector is required."
// InvalidSetWeightMessage indicates the setweight value needs to be between 0 and 100
InvalidSetWeightMessage = "SetWeight needs to be between 0 and 100"
// InvalidDurationMessage indicates the Duration value needs to be greater than 0
InvalidDurationMessage = "Duration needs to be greater than 0"
// InvalidMaxSurgeMaxUnavailable indicates both maxSurge and MaxUnavailable can not be set to zero
InvalidMaxSurgeMaxUnavailable = "MaxSurge and MaxUnavailable both can not be zero"
// InvalidStepMessage indicates that a step must have either setWeight or pause set
InvalidStepMessage = "Step must have either setWeight or pause set"
// ScaleDownDelayLongerThanDeadlineMessage indicates the ScaleDownDelaySeconds is longer than ProgressDeadlineSeconds
ScaleDownDelayLongerThanDeadlineMessage = "ScaleDownDelaySeconds cannot be longer than ProgressDeadlineSeconds"
// MinReadyLongerThanDeadlineMessage indicates the MinReadySeconds is longer than ProgressDeadlineSeconds
MinReadyLongerThanDeadlineMessage = "MinReadySeconds cannot be longer than ProgressDeadlineSeconds"
// InvalidStrategyMessage indiciates that multiple strategies can not be listed
InvalidStrategyMessage = "Multiple Strategies can not be listed"
// DuplicatedServicesMessage the message to indicate that the rollout uses the same service for the active and preview services
DuplicatedServicesMessage = "This rollout uses the same service for the active and preview services, but two different services are required."
// AvailableReason the reason to indicate that the rollout is serving traffic from the active service
AvailableReason = "AvailableReason"
// NotAvailableMessage the message to indicate that the Rollout does not have min availability
NotAvailableMessage = "Rollout does not have minimum availability"
// AvailableMessage the message to indicate that the Rollout does have min availability
AvailableMessage = "Rollout has minimum availability"
// Reasons and Messages for rollout Progressing Condition
// ReplicaSetUpdatedReason is added in a rollout when one of its replica sets is updated as part
// of the rollout process.
ReplicaSetUpdatedReason = "ReplicaSetUpdated"
// RolloutProgessingMessage is added in a rollout when one of its replica sets is updated as part
// of the rollout process.
RolloutProgressingMessage = "Rollout %q is progressing."
// ReplicaSetProgessingMessage is added in a rollout when one of its replica sets is updated as part
// of the rollout process.
ReplicaSetProgressingMessage = "ReplicaSet %q is progressing."
// FailedRSCreateReason is added in a rollout when it cannot create a new replica set.
FailedRSCreateReason = "ReplicaSetCreateError"
// FailedRSCreateMessage is added in a rollout when it cannot create a new replica set.
FailedRSCreateMessage = "Failed to create new replica set %q: %v"
// NewReplicaSetReason is added in a rollout when it creates a new replica set.
NewReplicaSetReason = "NewReplicaSetCreated"
//NewReplicasSetMessage is added in a rollout when it creates a new replicas \set.
NewReplicaSetMessage = "Created new replica set %q"
// FoundNewRSReason is added in a rollout when it adopts an existing replica set.
FoundNewRSReason = "FoundNewReplicaSet"
// FoundNewRSMessage is added in a rollout when it adopts an existing replica set.
FoundNewRSMessage = "Found new replica set %q"
// NewRSAvailableReason is added in a rollout when its newest replica set is made available
// ie. the number of new pods that have passed readiness checks and run for at least minReadySeconds
// is at least the minimum available pods that need to run for the rollout.
NewRSAvailableReason = "NewReplicaSetAvailable"
// TimedOutReason is added in a rollout when its newest replica set fails to show any progress
// within the given deadline (progressDeadlineSeconds).
TimedOutReason = "ProgressDeadlineExceeded"
// RolloutTimeOutMessage is is added in a rollout when the rollout fails to show any progress
// within the given deadline (progressDeadlineSeconds).
RolloutTimeOutMessage = "Rollout %q has timed out progressing."
// ReplicaSetTimeOutMessage is added in a rollout when its newest replica set fails to show any progress
// within the given deadline (progressDeadlineSeconds).
ReplicaSetTimeOutMessage = "ReplicaSet %q has timed out progressing."
// RolloutCompletedMessage is added when the rollout is completed
RolloutCompletedMessage = "Rollout %q has successfully progressed."
// ReplicaSetCompletedMessage is added when the rollout is completed
ReplicaSetCompletedMessage = "ReplicaSet %q has successfully progressed."
// PausedRolloutReason is added in a rollout when it is paused. Lack of progress shouldn't be
// estimated once a rollout is paused.
PausedRolloutReason = "RolloutPaused"
// PausedRolloutMessage is added in a rollout when it is paused. Lack of progress shouldn't be
// estimated once a rollout is paused.
PausedRolloutMessage = "Rollout is paused"
// ResumedRolloutReason is added in a rollout when it is resumed. Useful for not failing accidentally
// rollout that paused amidst a rollout and are bounded by a deadline.
ResumedRolloutReason = "RolloutResumed"
// ResumeRolloutMessage is added in a rollout when it is resumed. Useful for not failing accidentally
// rollout that paused amidst a rollout and are bounded by a deadline.
ResumeRolloutMessage = "Rollout is resumed"
// ServiceNotFoundReason is added in a rollout when the service defined in the spec is not found
ServiceNotFoundReason = "ServiceNotFound"
// ServiceNotFoundMessage is added in a rollout when the service defined in the spec is not found
ServiceNotFoundMessage = "Service %q is not found"
)
// NewRolloutCondition creates a new rollout condition.
func NewRolloutCondition(condType v1alpha1.RolloutConditionType, status corev1.ConditionStatus, reason, message string) *v1alpha1.RolloutCondition {
return &v1alpha1.RolloutCondition{
Type: condType,
Status: status,
LastUpdateTime: metav1.Now(),
LastTransitionTime: metav1.Now(),
Reason: reason,
Message: message,
}
}
// GetRolloutCondition returns the condition with the provided type.
func GetRolloutCondition(status v1alpha1.RolloutStatus, condType v1alpha1.RolloutConditionType) *v1alpha1.RolloutCondition {
for i := range status.Conditions {
c := status.Conditions[i]
if c.Type == condType {
return &c
}
}
return nil
}
// SetRolloutCondition updates the rollout to include the provided condition. If the condition that
// we are about to add already exists and has the same status and reason then we are not going to update.
func SetRolloutCondition(status *v1alpha1.RolloutStatus, condition v1alpha1.RolloutCondition) {
currentCond := GetRolloutCondition(*status, condition.Type)
if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason {
return
}
// Do not update lastTransitionTime if the status of the condition doesn't change.
if currentCond != nil && currentCond.Status == condition.Status {
condition.LastTransitionTime = currentCond.LastTransitionTime
}
newConditions := filterOutCondition(status.Conditions, condition.Type)
status.Conditions = append(newConditions, condition)
}
// RemoveRolloutCondition removes the rollout condition with the provided type.
func RemoveRolloutCondition(status *v1alpha1.RolloutStatus, condType v1alpha1.RolloutConditionType) {
status.Conditions = filterOutCondition(status.Conditions, condType)
}
// filterOutCondition returns a new slice of rollout conditions without conditions with the provided type.
func filterOutCondition(conditions []v1alpha1.RolloutCondition, condType v1alpha1.RolloutConditionType) []v1alpha1.RolloutCondition {
var newConditions []v1alpha1.RolloutCondition
for _, c := range conditions {
if c.Type == condType {
continue
}
newConditions = append(newConditions, c)
}
return newConditions
}
// RolloutProgressing reports progress for a rollout. Progress is estimated by comparing the
// current with the new status of the rollout that the controller is observing. More specifically,
// when new pods are scaled up, become ready or available, old pods are scaled down, or we modify the
// services, then we consider the rollout is progressing.
func RolloutProgressing(rollout *v1alpha1.Rollout, newStatus *v1alpha1.RolloutStatus) bool {
oldStatus := rollout.Status
strategySpecificProgress := false
if rollout.Spec.Strategy.BlueGreenStrategy != nil {
activeSelectorChange := rollout.Status.BlueGreen.ActiveSelector != newStatus.BlueGreen.ActiveSelector
previewSelectorChange := rollout.Status.BlueGreen.PreviewSelector != newStatus.BlueGreen.PreviewSelector
strategySpecificProgress = activeSelectorChange || previewSelectorChange
}
if rollout.Spec.Strategy.CanaryStrategy != nil {
stableRSChange := newStatus.Canary.StableRS != oldStatus.Canary.StableRS
incrementStepIndex := false
if newStatus.CurrentStepIndex != nil && oldStatus.CurrentStepIndex != nil {
incrementStepIndex = *newStatus.CurrentStepIndex != *oldStatus.CurrentStepIndex
}
stepsHashChange := newStatus.CurrentStepHash != oldStatus.CurrentStepHash
strategySpecificProgress = stableRSChange || incrementStepIndex || stepsHashChange
}
// Old replicas that need to be scaled down
oldStatusOldReplicas := oldStatus.Replicas - oldStatus.UpdatedReplicas
newStatusOldReplicas := newStatus.Replicas - newStatus.UpdatedReplicas
return (newStatus.UpdatedReplicas != oldStatus.UpdatedReplicas) ||
(newStatusOldReplicas < oldStatusOldReplicas) ||
newStatus.ReadyReplicas > rollout.Status.ReadyReplicas ||
newStatus.AvailableReplicas > rollout.Status.AvailableReplicas ||
strategySpecificProgress
}
// RolloutComplete considers a rollout to be complete once all of its desired replicas
// are updated, available, and receiving traffic from the active service, and no old pods are running.
func RolloutComplete(rollout *v1alpha1.Rollout, newStatus *v1alpha1.RolloutStatus) bool {
completedStrategy := true
if rollout.Spec.Strategy.BlueGreenStrategy != nil {
activeSelectorComplete := newStatus.BlueGreen.ActiveSelector == newStatus.CurrentPodHash
previewSelectorComplete := true
if rollout.Spec.Strategy.BlueGreenStrategy.PreviewService != "" {
previewSelectorComplete = newStatus.BlueGreen.PreviewSelector == ""
}
completedStrategy = activeSelectorComplete && previewSelectorComplete
}
if rollout.Spec.Strategy.CanaryStrategy != nil {
stepCount := len(rollout.Spec.Strategy.CanaryStrategy.Steps)
executedAllSteps := true
if stepCount > 0 && newStatus.CurrentStepIndex != nil {
executedAllSteps = int32(stepCount) == *newStatus.CurrentStepIndex
}
currentRSIsStable := newStatus.Canary.StableRS == controller.ComputeHash(&rollout.Spec.Template, rollout.Status.CollisionCount)
completedStrategy = executedAllSteps && currentRSIsStable
}
replicas := defaults.GetRolloutReplicasOrDefault(rollout)
return newStatus.UpdatedReplicas == replicas &&
newStatus.Replicas == replicas &&
newStatus.AvailableReplicas == replicas &&
rollout.Status.ObservedGeneration == ComputeGenerationHash(rollout.Spec) &&
completedStrategy
}
// ComputeStepHash returns a hash value calculated from the Rollout's steps. The hash will
// be safe encoded to avoid bad words.
func ComputeStepHash(rollout *v1alpha1.Rollout) string {
rolloutStepHasher := fnv.New32a()
if rollout.Spec.Strategy.BlueGreenStrategy != nil {
return ""
}
if rollout.Spec.Strategy.CanaryStrategy != nil {
hashutil.DeepHashObject(rolloutStepHasher, rollout.Spec.Strategy.CanaryStrategy.Steps)
}
return rand.SafeEncodeString(fmt.Sprint(rolloutStepHasher.Sum32()))
}
// ComputeGenerationHash returns a hash value calculated from the Rollout Spec. The hash will
// be safe encoded to avoid bad words.
func ComputeGenerationHash(spec v1alpha1.RolloutSpec) string {
rolloutSpecHasher := fnv.New32a()
hashutil.DeepHashObject(rolloutSpecHasher, spec)
return rand.SafeEncodeString(fmt.Sprint(rolloutSpecHasher.Sum32()))
}
func newInvalidSpecRolloutCondition(prevCond *v1alpha1.RolloutCondition, reason string, message string) *v1alpha1.RolloutCondition {
if prevCond != nil && prevCond.Message == message {
prevCond.LastUpdateTime = metav1.Now()
return prevCond
}
return NewRolloutCondition(v1alpha1.InvalidSpec, corev1.ConditionTrue, reason, message)
}
// VerifyRolloutSpec Checks for a valid spec otherwise returns a invalidSpec condition.
func VerifyRolloutSpec(rollout *v1alpha1.Rollout, prevCond *v1alpha1.RolloutCondition) *v1alpha1.RolloutCondition {
if rollout.Spec.Selector == nil {
message := fmt.Sprintf(MissingFieldMessage, ".Spec.Selector")
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, message)
}
everything := metav1.LabelSelector{}
if reflect.DeepEqual(rollout.Spec.Selector, &everything) {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, SelectAllMessage)
}
if rollout.Spec.Strategy.CanaryStrategy == nil && rollout.Spec.Strategy.BlueGreenStrategy == nil {
message := fmt.Sprintf(MissingFieldMessage, ".Spec.Strategy.CanaryStrategy or .Spec.Strategy.BlueGreen")
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, message)
}
if rollout.Spec.Strategy.CanaryStrategy != nil && rollout.Spec.Strategy.BlueGreenStrategy != nil {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, InvalidStrategyMessage)
}
if rollout.Spec.MinReadySeconds > defaults.GetProgressDeadlineSecondsOrDefault(rollout) {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, MinReadyLongerThanDeadlineMessage)
}
if rollout.Spec.Strategy.BlueGreenStrategy != nil {
if defaults.GetScaleDownDelaySecondsOrDefault(rollout) > defaults.GetProgressDeadlineSecondsOrDefault(rollout) {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, ScaleDownDelayLongerThanDeadlineMessage)
}
if rollout.Spec.Strategy.BlueGreenStrategy.ActiveService == "" {
message := fmt.Sprintf(MissingFieldMessage, ".Spec.Strategy.BlueGreenStrategy.ActiveService")
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, message)
}
if rollout.Spec.Strategy.BlueGreenStrategy.ActiveService == rollout.Spec.Strategy.BlueGreenStrategy.PreviewService {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, DuplicatedServicesMessage)
}
}
if rollout.Spec.Strategy.CanaryStrategy != nil {
maxSurge := rollout.Spec.Strategy.CanaryStrategy.MaxSurge
maxUnavailable := rollout.Spec.Strategy.CanaryStrategy.MaxUnavailable
if maxSurge != nil && maxUnavailable != nil {
if maxSurge.IntValue() == maxUnavailable.IntValue() && maxSurge.IntValue() == 0 {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, InvalidMaxSurgeMaxUnavailable)
}
}
for _, step := range rollout.Spec.Strategy.CanaryStrategy.Steps {
if (step.Pause != nil && step.SetWeight != nil) || (step.Pause == nil && step.SetWeight == nil) {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, InvalidStepMessage)
}
if step.SetWeight != nil && (*step.SetWeight < 0 || *step.SetWeight > 100) {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, InvalidSetWeightMessage)
}
if step.Pause != nil && step.Pause.Duration != nil && *step.Pause.Duration < 0 {
return newInvalidSpecRolloutCondition(prevCond, InvalidSpecReason, InvalidDurationMessage)
}
}
}
return nil
}
// HasRevisionHistoryLimit checks if the RevisionHistoryLimit field is set
func HasRevisionHistoryLimit(r *v1alpha1.Rollout) bool {
return r.Spec.RevisionHistoryLimit != nil && *r.Spec.RevisionHistoryLimit != math.MaxInt32
}
// RolloutTimedOut considers a rollout to have timed out once its condition that reports progress
// is older than progressDeadlineSeconds or a Progressing condition with a TimedOutReason reason already
// exists.
func RolloutTimedOut(rollout *v1alpha1.Rollout, newStatus *v1alpha1.RolloutStatus) bool {
// Look for the Progressing condition. If it doesn't exist, we have no base to estimate progress.
// If it's already set with a TimedOutReason reason, we have already timed out, no need to check
// again.
condition := GetRolloutCondition(*newStatus, v1alpha1.RolloutProgressing)
if condition == nil {
return false
}
// If the previous condition has been a successful rollout then we shouldn't try to
// estimate any progress. Scenario:
//
// * progressDeadlineSeconds is smaller than the difference between now and the time
// the last rollout finished in the past.
// * the creation of a new ReplicaSet triggers a resync of the rollout prior to the
// cached copy of the Rollout getting updated with the status.condition that indicates
// the creation of the new ReplicaSet.
//
// The rollout will be resynced and eventually its Progressing condition will catch
// up with the state of the world.
if condition.Reason == NewRSAvailableReason {
return false
}
if condition.Reason == TimedOutReason {
return true
}
// Look at the difference in seconds between now and the last time we reported any
// progress or tried to create a replica set, or resumed a paused rollout and
// compare against progressDeadlineSeconds.
from := condition.LastUpdateTime
now := time.Now()
progressDeadlineSeconds := defaults.GetProgressDeadlineSecondsOrDefault(rollout)
delta := time.Duration(progressDeadlineSeconds) * time.Second
timedOut := from.Add(delta).Before(now)
logCtx := logutil.WithRollout(rollout)
logCtx.Infof("Timed out (%t) [last progress check: %v - now: %v]", timedOut, from, now)
return timedOut
}
// ReplicaSetToRolloutCondition converts a replica set condition into a rollout condition.
// Useful for promoting replica set failure conditions into rollout.
func ReplicaSetToRolloutCondition(cond appsv1.ReplicaSetCondition) v1alpha1.RolloutCondition {
return v1alpha1.RolloutCondition{
Type: v1alpha1.RolloutConditionType(cond.Type),
Status: cond.Status,
LastTransitionTime: cond.LastTransitionTime,
LastUpdateTime: cond.LastTransitionTime,
Reason: cond.Reason,
Message: cond.Message,
}
}