Skip to content
This repository was archived by the owner on Feb 8, 2021. It is now read-only.

Commit 77097d9

Browse files
committed
kubectl: set maxUnavailable to 1 if both fenceposts resolve to zero
Due to rounding down for maxUnavailable, we may end up with rolling updates that have zero surge and unavailable pods something that 1) is not allowed as per validation, 2) blocks updates. If we end up in such a situation set maxUnavailable to 1 on the theory that surge might not work due to quota.
1 parent e4a3875 commit 77097d9

File tree

3 files changed

+78
-9
lines changed

3 files changed

+78
-9
lines changed

pkg/kubectl/rolling_updater.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
client "k8s.io/kubernetes/pkg/client/unversioned"
3030
"k8s.io/kubernetes/pkg/labels"
3131
"k8s.io/kubernetes/pkg/runtime"
32+
"k8s.io/kubernetes/pkg/util/deployment"
3233
"k8s.io/kubernetes/pkg/util/integer"
3334
"k8s.io/kubernetes/pkg/util/intstr"
3435
"k8s.io/kubernetes/pkg/util/wait"
@@ -191,13 +192,9 @@ func (r *RollingUpdater) Update(config *RollingUpdaterConfig) error {
191192
}
192193
oldRc = updated
193194
}
194-
// The maximum pods which can go unavailable during the update.
195-
maxUnavailable, err := intstr.GetValueFromIntOrPercent(&config.MaxUnavailable, desired, false)
196-
if err != nil {
197-
return err
198-
}
199-
// The maximum scaling increment.
200-
maxSurge, err := intstr.GetValueFromIntOrPercent(&config.MaxSurge, desired, true)
195+
// maxSurge is the maximum scaling increment and maxUnavailable are the maximum pods
196+
// that can be unavailable during a rollout.
197+
maxSurge, maxUnavailable, err := deployment.ResolveFenceposts(&config.MaxSurge, &config.MaxUnavailable, desired)
201198
if err != nil {
202199
return err
203200
}

pkg/kubectl/rolling_updater_test.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,11 +662,11 @@ Scaling foo-v2 up to 2
662662
`,
663663
},
664664
{
665-
name: "1->1 100%/0 allow maxUnavailability",
665+
name: "1->1 1/0 allow maxUnavailability",
666666
oldRc: oldRc(1, 1),
667667
newRc: newRc(0, 1),
668668
newRcExists: false,
669-
maxUnavail: intstr.FromString("100%"),
669+
maxUnavail: intstr.FromString("1%"),
670670
maxSurge: intstr.FromInt(0),
671671
expected: []interface{}{
672672
down{oldReady: 1, newReady: 0, to: 0},
@@ -693,6 +693,48 @@ Scaling foo-v2 up to 1
693693
Scaling up foo-v2 from 0 to 2, scaling down foo-v1 from 1 to 0 (keep 2 pods available, don't exceed 3 pods)
694694
Scaling foo-v2 up to 2
695695
Scaling foo-v1 down to 0
696+
`,
697+
},
698+
{
699+
name: "2->2 25/1 maxSurge trumps maxUnavailable",
700+
oldRc: oldRc(2, 2),
701+
newRc: newRc(0, 2),
702+
newRcExists: false,
703+
maxUnavail: intstr.FromString("25%"),
704+
maxSurge: intstr.FromString("1%"),
705+
expected: []interface{}{
706+
up{1},
707+
down{oldReady: 2, newReady: 1, to: 1},
708+
up{2},
709+
down{oldReady: 1, newReady: 2, to: 0},
710+
},
711+
output: `Created foo-v2
712+
Scaling up foo-v2 from 0 to 2, scaling down foo-v1 from 2 to 0 (keep 2 pods available, don't exceed 3 pods)
713+
Scaling foo-v2 up to 1
714+
Scaling foo-v1 down to 1
715+
Scaling foo-v2 up to 2
716+
Scaling foo-v1 down to 0
717+
`,
718+
},
719+
{
720+
name: "2->2 25/0 maxUnavailable resolves to zero, then one",
721+
oldRc: oldRc(2, 2),
722+
newRc: newRc(0, 2),
723+
newRcExists: false,
724+
maxUnavail: intstr.FromString("25%"),
725+
maxSurge: intstr.FromString("0%"),
726+
expected: []interface{}{
727+
down{oldReady: 2, newReady: 0, to: 1},
728+
up{1},
729+
down{oldReady: 1, newReady: 1, to: 0},
730+
up{2},
731+
},
732+
output: `Created foo-v2
733+
Scaling up foo-v2 from 0 to 2, scaling down foo-v1 from 2 to 0 (keep 1 pods available, don't exceed 2 pods)
734+
Scaling foo-v1 down to 1
735+
Scaling foo-v2 up to 1
736+
Scaling foo-v1 down to 0
737+
Scaling foo-v2 up to 2
696738
`,
697739
},
698740
}

pkg/util/deployment/deployment.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,3 +466,33 @@ func WaitForObservedDeployment(getDeploymentFunc func() (*extensions.Deployment,
466466
return deployment.Status.ObservedGeneration >= desiredGeneration, nil
467467
})
468468
}
469+
470+
// ResolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one
471+
// step. For example:
472+
//
473+
// 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1)
474+
// 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1)
475+
// 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
476+
// 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1)
477+
// 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
478+
// 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1)
479+
func ResolveFenceposts(maxSurge, maxUnavailable *intstrutil.IntOrString, desired int) (int, int, error) {
480+
surge, err := intstrutil.GetValueFromIntOrPercent(maxSurge, desired, true)
481+
if err != nil {
482+
return 0, 0, err
483+
}
484+
unavailable, err := intstrutil.GetValueFromIntOrPercent(maxUnavailable, desired, false)
485+
if err != nil {
486+
return 0, 0, err
487+
}
488+
489+
if surge == 0 && unavailable == 0 {
490+
// Validation should never allow the user to explicitly use zero values for both maxSurge
491+
// maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero.
492+
// If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the
493+
// theory that surge might not work due to quota.
494+
unavailable = 1
495+
}
496+
497+
return surge, unavailable, nil
498+
}

0 commit comments

Comments
 (0)