API: maxUnavailable for StatefulSet

This commit is contained in:
Mayank Kumar
2019-08-30 00:05:18 -07:00
parent e0ca5cfd73
commit 357203d992
46 changed files with 1369 additions and 484 deletions

View File

@@ -92,9 +92,20 @@ const (
// RollingUpdateStatefulSetStrategy is used to communicate parameter for RollingUpdateStatefulSetStrategyType.
type RollingUpdateStatefulSetStrategy struct {
// Partition indicates the ordinal at which the StatefulSet should be
// partitioned.
// Partition indicates the ordinal at which the StatefulSet should be partitioned
// for updates. During a rolling update, all pods from ordinal Replicas-1 to
// Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched.
// This is helpful in being able to do a canary based deployment. The default value is 0.
Partition int32
// The maximum number of pods that can be unavailable during the update.
// Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%).
// Absolute number is calculated from percentage by rounding up. This can not be 0.
// Defaults to 1. This field is alpha-level and is only honored by servers that enable the
// MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to
// Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it
// will be counted towards MaxUnavailable.
// +optional
MaxUnavailable *intstr.IntOrString
}
// PersistentVolumeClaimRetentionPolicyType is a string enumeration of the policies that will determine

View File

@@ -113,10 +113,18 @@ func SetDefaults_StatefulSet(obj *appsv1.StatefulSet) {
}
if obj.Spec.UpdateStrategy.Type == appsv1.RollingUpdateStatefulSetStrategyType &&
obj.Spec.UpdateStrategy.RollingUpdate != nil &&
obj.Spec.UpdateStrategy.RollingUpdate.Partition == nil {
obj.Spec.UpdateStrategy.RollingUpdate.Partition = new(int32)
*obj.Spec.UpdateStrategy.RollingUpdate.Partition = 0
obj.Spec.UpdateStrategy.RollingUpdate != nil {
if obj.Spec.UpdateStrategy.RollingUpdate.Partition == nil {
obj.Spec.UpdateStrategy.RollingUpdate.Partition = new(int32)
*obj.Spec.UpdateStrategy.RollingUpdate.Partition = 0
}
if utilfeature.DefaultFeatureGate.Enabled(features.MaxUnavailableStatefulSet) {
if obj.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable == nil {
maxUnavailable := intstr.FromInt(1)
obj.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable = &maxUnavailable
}
}
}
if utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetAutoDeletePVC) {

View File

@@ -175,6 +175,15 @@ func TestSetDefaultDaemonSetSpec(t *testing.T) {
}
}
func getMaxUnavailable(maxUnavailable int) *intstr.IntOrString {
maxUnavailableIntOrStr := intstr.FromInt(maxUnavailable)
return &maxUnavailableIntOrStr
}
func getPartition(partition int32) *int32 {
return &partition
}
func TestSetDefaultStatefulSet(t *testing.T) {
defaultLabels := map[string]string{"foo": "bar"}
var defaultPartition int32 = 0
@@ -196,10 +205,11 @@ func TestSetDefaultStatefulSet(t *testing.T) {
}
tests := []struct {
name string
original *appsv1.StatefulSet
expected *appsv1.StatefulSet
enablePVCDeletionPolicy bool
name string
original *appsv1.StatefulSet
expected *appsv1.StatefulSet
enablePVCDeletionPolicy bool
enableMaxUnavailablePolicy bool
}{
{
name: "labels and default update strategy",
@@ -439,12 +449,165 @@ func TestSetDefaultStatefulSet(t *testing.T) {
},
enablePVCDeletionPolicy: false,
},
{
name: "MaxUnavailable disabled, with maxUnavailable not specified",
original: &appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: defaultTemplate,
},
},
expected: &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
Type: appsv1.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: getPartition(0),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: false,
},
{
name: "MaxUnavailable disabled, with default maxUnavailable specified",
original: &appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: defaultTemplate,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: &defaultPartition,
MaxUnavailable: getMaxUnavailable(1),
},
},
},
},
expected: &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
Type: appsv1.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: getPartition(0),
MaxUnavailable: getMaxUnavailable(1),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: false,
},
{
name: "MaxUnavailable disabled, with non default maxUnavailable specified",
original: &appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: defaultTemplate,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: &notTheDefaultPartition,
MaxUnavailable: getMaxUnavailable(3),
},
},
},
},
expected: &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
Type: appsv1.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: getPartition(42),
MaxUnavailable: getMaxUnavailable(3),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: false,
},
{
name: "MaxUnavailable enabled, with no maxUnavailable specified",
original: &appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: defaultTemplate,
},
},
expected: &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
Type: appsv1.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: getPartition(0),
MaxUnavailable: getMaxUnavailable(1),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: true,
},
{
name: "MaxUnavailable enabled, with non default maxUnavailable specified",
original: &appsv1.StatefulSet{
Spec: appsv1.StatefulSetSpec{
Template: defaultTemplate,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: &notTheDefaultPartition,
MaxUnavailable: getMaxUnavailable(3),
},
},
},
},
expected: &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1.OrderedReadyPodManagement,
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
Type: appsv1.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{
Partition: getPartition(42),
MaxUnavailable: getMaxUnavailable(3),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StatefulSetAutoDeletePVC, test.enablePVCDeletionPolicy)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MaxUnavailableStatefulSet, test.enableMaxUnavailablePolicy)()
obj2 := roundTrip(t, runtime.Object(test.original))
got, ok := obj2.(*appsv1.StatefulSet)

View File

@@ -29,6 +29,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
intstr "k8s.io/apimachinery/pkg/util/intstr"
apps "k8s.io/kubernetes/pkg/apis/apps"
core "k8s.io/kubernetes/pkg/apis/core"
apiscorev1 "k8s.io/kubernetes/pkg/apis/core/v1"
@@ -1042,6 +1043,7 @@ func autoConvert_v1_RollingUpdateStatefulSetStrategy_To_apps_RollingUpdateStatef
if err := metav1.Convert_Pointer_int32_To_int32(&in.Partition, &out.Partition, s); err != nil {
return err
}
out.MaxUnavailable = (*intstr.IntOrString)(unsafe.Pointer(in.MaxUnavailable))
return nil
}
@@ -1054,6 +1056,7 @@ func autoConvert_apps_RollingUpdateStatefulSetStrategy_To_v1_RollingUpdateStatef
if err := metav1.Convert_int32_To_Pointer_int32(&in.Partition, &out.Partition, s); err != nil {
return err
}
out.MaxUnavailable = (*intstr.IntOrString)(unsafe.Pointer(in.MaxUnavailable))
return nil
}

View File

@@ -70,10 +70,18 @@ func SetDefaults_StatefulSet(obj *appsv1beta1.StatefulSet) {
*obj.Spec.RevisionHistoryLimit = 10
}
if obj.Spec.UpdateStrategy.Type == appsv1beta1.RollingUpdateStatefulSetStrategyType &&
obj.Spec.UpdateStrategy.RollingUpdate != nil &&
obj.Spec.UpdateStrategy.RollingUpdate.Partition == nil {
obj.Spec.UpdateStrategy.RollingUpdate.Partition = new(int32)
*obj.Spec.UpdateStrategy.RollingUpdate.Partition = 0
obj.Spec.UpdateStrategy.RollingUpdate != nil {
if obj.Spec.UpdateStrategy.RollingUpdate.Partition == nil {
obj.Spec.UpdateStrategy.RollingUpdate.Partition = new(int32)
*obj.Spec.UpdateStrategy.RollingUpdate.Partition = 0
}
if utilfeature.DefaultFeatureGate.Enabled(features.MaxUnavailableStatefulSet) {
if obj.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable == nil {
maxUnavailable := intstr.FromInt(1)
obj.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable = &maxUnavailable
}
}
}
}

View File

@@ -29,6 +29,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
intstr "k8s.io/apimachinery/pkg/util/intstr"
apps "k8s.io/kubernetes/pkg/apis/apps"
autoscaling "k8s.io/kubernetes/pkg/apis/autoscaling"
core "k8s.io/kubernetes/pkg/apis/core"
@@ -641,6 +642,7 @@ func autoConvert_v1beta1_RollingUpdateStatefulSetStrategy_To_apps_RollingUpdateS
if err := metav1.Convert_Pointer_int32_To_int32(&in.Partition, &out.Partition, s); err != nil {
return err
}
out.MaxUnavailable = (*intstr.IntOrString)(unsafe.Pointer(in.MaxUnavailable))
return nil
}
@@ -653,6 +655,7 @@ func autoConvert_apps_RollingUpdateStatefulSetStrategy_To_v1beta1_RollingUpdateS
if err := metav1.Convert_int32_To_Pointer_int32(&in.Partition, &out.Partition, s); err != nil {
return err
}
out.MaxUnavailable = (*intstr.IntOrString)(unsafe.Pointer(in.MaxUnavailable))
return nil
}

View File

@@ -63,15 +63,25 @@ func SetDefaults_StatefulSet(obj *appsv1beta2.StatefulSet) {
if obj.Spec.UpdateStrategy.Type == "" {
obj.Spec.UpdateStrategy.Type = appsv1beta2.RollingUpdateStatefulSetStrategyType
// UpdateStrategy.RollingUpdate will take default values below.
obj.Spec.UpdateStrategy.RollingUpdate = &appsv1beta2.RollingUpdateStatefulSetStrategy{}
if obj.Spec.UpdateStrategy.RollingUpdate == nil {
// UpdateStrategy.RollingUpdate will take default values below.
obj.Spec.UpdateStrategy.RollingUpdate = &appsv1beta2.RollingUpdateStatefulSetStrategy{}
}
}
if obj.Spec.UpdateStrategy.Type == appsv1beta2.RollingUpdateStatefulSetStrategyType &&
obj.Spec.UpdateStrategy.RollingUpdate != nil &&
obj.Spec.UpdateStrategy.RollingUpdate.Partition == nil {
obj.Spec.UpdateStrategy.RollingUpdate.Partition = new(int32)
*obj.Spec.UpdateStrategy.RollingUpdate.Partition = 0
obj.Spec.UpdateStrategy.RollingUpdate != nil {
if obj.Spec.UpdateStrategy.RollingUpdate.Partition == nil {
obj.Spec.UpdateStrategy.RollingUpdate.Partition = new(int32)
*obj.Spec.UpdateStrategy.RollingUpdate.Partition = 0
}
if utilfeature.DefaultFeatureGate.Enabled(features.MaxUnavailableStatefulSet) {
if obj.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable == nil {
maxUnavailable := intstr.FromInt(1)
obj.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable = &maxUnavailable
}
}
}
if utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetAutoDeletePVC) {

View File

@@ -27,10 +27,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/api/legacyscheme"
_ "k8s.io/kubernetes/pkg/apis/apps/install"
. "k8s.io/kubernetes/pkg/apis/apps/v1beta2"
_ "k8s.io/kubernetes/pkg/apis/core/install"
"k8s.io/kubernetes/pkg/features"
utilpointer "k8s.io/utils/pointer"
)
@@ -172,9 +175,19 @@ func TestSetDefaultDaemonSetSpec(t *testing.T) {
}
}
func getMaxUnavailable(maxUnavailable int) *intstr.IntOrString {
maxUnavailableIntOrStr := intstr.FromInt(maxUnavailable)
return &maxUnavailableIntOrStr
}
func getPartition(partition int32) *int32 {
return &partition
}
func TestSetDefaultStatefulSet(t *testing.T) {
defaultLabels := map[string]string{"foo": "bar"}
var defaultPartition int32 = 0
var notTheDefaultPartition int32 = 42
var defaultReplicas int32 = 1
period := int64(v1.DefaultTerminationGracePeriodSeconds)
@@ -192,10 +205,13 @@ func TestSetDefaultStatefulSet(t *testing.T) {
}
tests := []struct {
original *appsv1beta2.StatefulSet
expected *appsv1beta2.StatefulSet
name string
original *appsv1beta2.StatefulSet
expected *appsv1beta2.StatefulSet
enableMaxUnavailablePolicy bool
}{
{ // labels and default update strategy
{
name: "labels and default update strategy",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
@@ -220,7 +236,8 @@ func TestSetDefaultStatefulSet(t *testing.T) {
},
},
},
{ // Alternate update strategy
{
name: "Alternate update strategy",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
@@ -245,7 +262,8 @@ func TestSetDefaultStatefulSet(t *testing.T) {
},
},
},
{ // Parallel pod management policy.
{
name: "Parallel pod management policy.",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
@@ -271,20 +289,174 @@ func TestSetDefaultStatefulSet(t *testing.T) {
},
},
},
{
name: "MaxUnavailable disabled, with maxUnavailable not specified",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
},
},
expected: &appsv1beta2.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1beta2.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1beta2.OrderedReadyPodManagement,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
Type: appsv1beta2.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: getPartition(0),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: false,
},
{
name: "MaxUnavailable disabled, with default maxUnavailable specified",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: &defaultPartition,
MaxUnavailable: getMaxUnavailable(1),
},
},
},
},
expected: &appsv1beta2.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1beta2.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1beta2.OrderedReadyPodManagement,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
Type: appsv1beta2.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: getPartition(0),
MaxUnavailable: getMaxUnavailable(1),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: false,
},
{
name: "MaxUnavailable disabled, with non default maxUnavailable specified",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: &notTheDefaultPartition,
MaxUnavailable: getMaxUnavailable(3),
},
},
},
},
expected: &appsv1beta2.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1beta2.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1beta2.OrderedReadyPodManagement,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
Type: appsv1beta2.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: getPartition(42),
MaxUnavailable: getMaxUnavailable(3),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: false,
},
{
name: "MaxUnavailable enabled, with no maxUnavailable specified",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
},
},
expected: &appsv1beta2.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1beta2.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1beta2.OrderedReadyPodManagement,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
Type: appsv1beta2.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: getPartition(0),
MaxUnavailable: getMaxUnavailable(1),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: true,
},
{
name: "MaxUnavailable enabled, with non default maxUnavailable specified",
original: &appsv1beta2.StatefulSet{
Spec: appsv1beta2.StatefulSetSpec{
Template: defaultTemplate,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: &notTheDefaultPartition,
MaxUnavailable: getMaxUnavailable(3),
},
},
},
},
expected: &appsv1beta2.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Labels: defaultLabels,
},
Spec: appsv1beta2.StatefulSetSpec{
Replicas: &defaultReplicas,
Template: defaultTemplate,
PodManagementPolicy: appsv1beta2.OrderedReadyPodManagement,
UpdateStrategy: appsv1beta2.StatefulSetUpdateStrategy{
Type: appsv1beta2.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &appsv1beta2.RollingUpdateStatefulSetStrategy{
Partition: getPartition(42),
MaxUnavailable: getMaxUnavailable(3),
},
},
RevisionHistoryLimit: utilpointer.Int32Ptr(10),
},
},
enableMaxUnavailablePolicy: true,
},
}
for i, test := range tests {
original := test.original
expected := test.expected
obj2 := roundTrip(t, runtime.Object(original))
got, ok := obj2.(*appsv1beta2.StatefulSet)
if !ok {
t.Errorf("(%d) unexpected object: %v", i, got)
t.FailNow()
}
if !apiequality.Semantic.DeepEqual(got.Spec, expected.Spec) {
t.Errorf("(%d) got different than expected\ngot:\n\t%+v\nexpected:\n\t%+v", i, got.Spec, expected.Spec)
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MaxUnavailableStatefulSet, test.enableMaxUnavailablePolicy)()
obj2 := roundTrip(t, runtime.Object(test.original))
got, ok := obj2.(*appsv1beta2.StatefulSet)
if !ok {
t.Errorf("unexpected object: %v", got)
t.FailNow()
}
if !apiequality.Semantic.DeepEqual(got.Spec, test.expected.Spec) {
t.Errorf("got different than expected\ngot:\n\t%+v\nexpected:\n\t%+v", got.Spec, test.expected.Spec)
}
})
}
}

View File

@@ -29,6 +29,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
intstr "k8s.io/apimachinery/pkg/util/intstr"
apps "k8s.io/kubernetes/pkg/apis/apps"
autoscaling "k8s.io/kubernetes/pkg/apis/autoscaling"
core "k8s.io/kubernetes/pkg/apis/core"
@@ -1073,6 +1074,7 @@ func autoConvert_v1beta2_RollingUpdateStatefulSetStrategy_To_apps_RollingUpdateS
if err := metav1.Convert_Pointer_int32_To_int32(&in.Partition, &out.Partition, s); err != nil {
return err
}
out.MaxUnavailable = (*intstr.IntOrString)(unsafe.Pointer(in.MaxUnavailable))
return nil
}
@@ -1085,6 +1087,7 @@ func autoConvert_apps_RollingUpdateStatefulSetStrategy_To_v1beta2_RollingUpdateS
if err := metav1.Convert_int32_To_Pointer_int32(&in.Partition, &out.Partition, s); err != nil {
return err
}
out.MaxUnavailable = (*intstr.IntOrString)(unsafe.Pointer(in.MaxUnavailable))
return nil
}

View File

@@ -115,10 +115,8 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
}
case apps.RollingUpdateStatefulSetStrategyType:
if spec.UpdateStrategy.RollingUpdate != nil {
allErrs = append(allErrs,
apivalidation.ValidateNonnegativeField(
int64(spec.UpdateStrategy.RollingUpdate.Partition),
fldPath.Child("updateStrategy").Child("rollingUpdate").Child("partition"))...)
allErrs = append(allErrs, validateRollingUpdateStatefulSet(spec.UpdateStrategy.RollingUpdate, fldPath.Child("updateStrategy", "rollingUpdate"))...)
}
default:
allErrs = append(allErrs,
@@ -419,6 +417,26 @@ func ValidateRollingUpdateDaemonSet(rollingUpdate *apps.RollingUpdateDaemonSet,
return allErrs
}
// validateRollingUpdateStatefulSet validates a given RollingUpdateStatefulSet.
func validateRollingUpdateStatefulSet(rollingUpdate *apps.RollingUpdateStatefulSetStrategy, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
fldPathMaxUn := fldPath.Child("maxUnavailable")
allErrs = append(allErrs,
apivalidation.ValidateNonnegativeField(
int64(rollingUpdate.Partition),
fldPath.Child("partition"))...)
if rollingUpdate.MaxUnavailable != nil {
allErrs = append(allErrs, ValidatePositiveIntOrPercent(*rollingUpdate.MaxUnavailable, fldPathMaxUn)...)
if getIntOrPercentValue(*rollingUpdate.MaxUnavailable) == 0 {
// MaxUnavailable cannot be 0.
allErrs = append(allErrs, field.Invalid(fldPathMaxUn, *rollingUpdate.MaxUnavailable, "cannot be 0"))
}
// Validate that MaxUnavailable is not more than 100%.
allErrs = append(allErrs, IsNotMoreThan100Percent(*rollingUpdate.MaxUnavailable, fldPathMaxUn)...)
}
return allErrs
}
// ValidateDaemonSetUpdateStrategy validates a given DaemonSetUpdateStrategy.
func ValidateDaemonSetUpdateStrategy(strategy *apps.DaemonSetUpdateStrategy, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}

View File

@@ -34,6 +34,10 @@ import (
"k8s.io/kubernetes/pkg/features"
)
func intStrAddr(intOrStr intstr.IntOrString) *intstr.IntOrString {
return &intOrStr
}
func TestValidateStatefulSet(t *testing.T) {
validLabels := map[string]string{"a": "b"}
validPodTemplate := api.PodTemplate{
@@ -142,6 +146,22 @@ func TestValidateStatefulSet(t *testing.T) {
},
},
},
"maxUnavailable with parallel pod management": {
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
Spec: apps.StatefulSetSpec{
PodManagementPolicy: apps.ParallelPodManagement,
Selector: &metav1.LabelSelector{MatchLabels: validLabels},
Template: validPodTemplate.Template,
Replicas: 3,
UpdateStrategy: apps.StatefulSetUpdateStrategy{
Type: apps.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &apps.RollingUpdateStatefulSetStrategy{
Partition: 2,
MaxUnavailable: intStrAddr(intstr.FromInt(2)),
},
},
},
},
}
for name, successCase := range successCases {
@@ -394,6 +414,51 @@ func TestValidateStatefulSet(t *testing.T) {
UpdateStrategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType},
},
},
"zero maxUnavailable": {
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
Spec: apps.StatefulSetSpec{
PodManagementPolicy: apps.OrderedReadyPodManagement,
Selector: &metav1.LabelSelector{MatchLabels: validLabels},
Template: validPodTemplate.Template,
Replicas: 3,
UpdateStrategy: apps.StatefulSetUpdateStrategy{
Type: apps.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &apps.RollingUpdateStatefulSetStrategy{
MaxUnavailable: intStrAddr(intstr.FromInt(0)),
},
},
},
},
"zero percent maxUnavailable": {
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
Spec: apps.StatefulSetSpec{
PodManagementPolicy: apps.ParallelPodManagement,
Selector: &metav1.LabelSelector{MatchLabels: validLabels},
Template: validPodTemplate.Template,
Replicas: 3,
UpdateStrategy: apps.StatefulSetUpdateStrategy{
Type: apps.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &apps.RollingUpdateStatefulSetStrategy{
MaxUnavailable: intStrAddr(intstr.FromString("0%")),
},
},
},
},
"greater than 100 percent maxUnavailable": {
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
Spec: apps.StatefulSetSpec{
PodManagementPolicy: apps.ParallelPodManagement,
Selector: &metav1.LabelSelector{MatchLabels: validLabels},
Template: validPodTemplate.Template,
Replicas: 3,
UpdateStrategy: apps.StatefulSetUpdateStrategy{
Type: apps.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &apps.RollingUpdateStatefulSetStrategy{
MaxUnavailable: intStrAddr(intstr.FromString("101%")),
},
},
},
},
}
for k, v := range errorCases {
@@ -426,6 +491,7 @@ func TestValidateStatefulSet(t *testing.T) {
field != "spec.persistentVolumeClaimRetentionPolicy" &&
field != "spec.persistentVolumeClaimRetentionPolicy.whenDeleted" &&
field != "spec.persistentVolumeClaimRetentionPolicy.whenScaled" &&
field != "spec.updateStrategy.rollingUpdate.maxUnavailable" &&
field != "spec.template.spec.activeDeadlineSeconds" {
t.Errorf("%s: missing prefix for: %v", k, errs[i])
}

View File

@@ -24,6 +24,7 @@ package apps
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
intstr "k8s.io/apimachinery/pkg/util/intstr"
core "k8s.io/kubernetes/pkg/apis/core"
)
@@ -621,6 +622,11 @@ func (in *RollingUpdateDeployment) DeepCopy() *RollingUpdateDeployment {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RollingUpdateStatefulSetStrategy) DeepCopyInto(out *RollingUpdateStatefulSetStrategy) {
*out = *in
if in.MaxUnavailable != nil {
in, out := &in.MaxUnavailable, &out.MaxUnavailable
*out = new(intstr.IntOrString)
**out = **in
}
return
}
@@ -807,7 +813,7 @@ func (in *StatefulSetUpdateStrategy) DeepCopyInto(out *StatefulSetUpdateStrategy
if in.RollingUpdate != nil {
in, out := &in.RollingUpdate, &out.RollingUpdate
*out = new(RollingUpdateStatefulSetStrategy)
**out = **in
(*in).DeepCopyInto(*out)
}
return
}

View File

@@ -858,6 +858,12 @@ const (
//
// Allow pods to failover to a different node in case of non graceful node shutdown
NodeOutOfServiceVolumeDetach featuregate.Feature = "NodeOutOfServiceVolumeDetach"
// owner: @krmayankk
// alpha: v1.24
//
// Enables maxUnavailable for StatefulSet
MaxUnavailableStatefulSet featuregate.Feature = "MaxUnavailableStatefulSet"
)
func init() {
@@ -982,6 +988,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
MinDomainsInPodTopologySpread: {Default: false, PreRelease: featuregate.Alpha},
ServiceIPStaticSubrange: {Default: false, PreRelease: featuregate.Alpha},
NodeOutOfServiceVolumeDetach: {Default: false, PreRelease: featuregate.Alpha},
MaxUnavailableStatefulSet: {Default: false, PreRelease: featuregate.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:
genericfeatures.AdvancedAuditing: {Default: true, PreRelease: featuregate.GA},

View File

@@ -3774,14 +3774,22 @@ func schema_k8sio_api_apps_v1_RollingUpdateStatefulSetStrategy(ref common.Refere
Properties: map[string]spec.Schema{
"partition": {
SchemaProps: spec.SchemaProps{
Description: "Partition indicates the ordinal at which the StatefulSet should be partitioned. Default value is 0.",
Description: "Partition indicates the ordinal at which the StatefulSet should be partitioned for updates. During a rolling update, all pods from ordinal Replicas-1 to Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. This is helpful in being able to do a canary based deployment. The default value is 0.",
Type: []string{"integer"},
Format: "int32",
},
},
"maxUnavailable": {
SchemaProps: spec.SchemaProps{
Description: "The maximum number of pods that can be unavailable during the update. Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). Absolute number is calculated from percentage by rounding up. This can not be 0. Defaults to 1. This field is alpha-level and is only honored by servers that enable the MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it will be counted towards MaxUnavailable.",
Ref: ref("k8s.io/apimachinery/pkg/util/intstr.IntOrString"),
},
},
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/util/intstr.IntOrString"},
}
}
@@ -4762,14 +4770,22 @@ func schema_k8sio_api_apps_v1beta1_RollingUpdateStatefulSetStrategy(ref common.R
Properties: map[string]spec.Schema{
"partition": {
SchemaProps: spec.SchemaProps{
Description: "Partition indicates the ordinal at which the StatefulSet should be partitioned.",
Description: "Partition indicates the ordinal at which the StatefulSet should be partitioned for updates. During a rolling update, all pods from ordinal Replicas-1 to Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. This is helpful in being able to do a canary based deployment. The default value is 0.",
Type: []string{"integer"},
Format: "int32",
},
},
"maxUnavailable": {
SchemaProps: spec.SchemaProps{
Description: "The maximum number of pods that can be unavailable during the update. Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). Absolute number is calculated from percentage by rounding up. This can not be 0. Defaults to 1. This field is alpha-level and is only honored by servers that enable the MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it will be counted towards MaxUnavailable.",
Ref: ref("k8s.io/apimachinery/pkg/util/intstr.IntOrString"),
},
},
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/util/intstr.IntOrString"},
}
}
@@ -6406,14 +6422,22 @@ func schema_k8sio_api_apps_v1beta2_RollingUpdateStatefulSetStrategy(ref common.R
Properties: map[string]spec.Schema{
"partition": {
SchemaProps: spec.SchemaProps{
Description: "Partition indicates the ordinal at which the StatefulSet should be partitioned. Default value is 0.",
Description: "Partition indicates the ordinal at which the StatefulSet should be partitioned for updates. During a rolling update, all pods from ordinal Replicas-1 to Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. This is helpful in being able to do a canary based deployment. The default value is 0.",
Type: []string{"integer"},
Format: "int32",
},
},
"maxUnavailable": {
SchemaProps: spec.SchemaProps{
Description: "The maximum number of pods that can be unavailable during the update. Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). Absolute number is calculated from percentage by rounding up. This can not be 0. Defaults to 1. This field is alpha-level and is only honored by servers that enable the MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it will be counted towards MaxUnavailable.",
Ref: ref("k8s.io/apimachinery/pkg/util/intstr.IntOrString"),
},
},
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/util/intstr.IntOrString"},
}
}

View File

@@ -79,6 +79,18 @@ func (statefulSetStrategy) PrepareForCreate(ctx context.Context, obj runtime.Obj
pod.DropDisabledTemplateFields(&statefulSet.Spec.Template, nil)
}
// maxUnavailableInUse returns true if StatefulSet's maxUnavailable set(used)
func maxUnavailableInUse(statefulset *apps.StatefulSet) bool {
if statefulset == nil {
return false
}
if statefulset.Spec.UpdateStrategy.RollingUpdate == nil {
return false
}
return statefulset.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable != nil
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (statefulSetStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newStatefulSet := obj.(*apps.StatefulSet)
@@ -115,6 +127,11 @@ func dropStatefulSetDisabledFields(newSS *apps.StatefulSet, oldSS *apps.Stateful
newSS.Spec.PersistentVolumeClaimRetentionPolicy = nil
}
}
if !utilfeature.DefaultFeatureGate.Enabled(features.MaxUnavailableStatefulSet) && !maxUnavailableInUse(oldSS) {
if newSS.Spec.UpdateStrategy.RollingUpdate != nil {
newSS.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable = nil
}
}
}
// minReadySecondsFieldsInUse returns true if fields related to StatefulSet minReadySeconds are set and

View File

@@ -17,11 +17,11 @@ limitations under the License.
package statefulset
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/util/intstr"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
@@ -353,11 +353,35 @@ func generateStatefulSetWithMinReadySeconds(minReadySeconds int32) *apps.Statefu
}
}
func makeStatefulSetWithMaxUnavailable(maxUnavailable *int) *apps.StatefulSet {
rollingUpdate := apps.RollingUpdateStatefulSetStrategy{}
if maxUnavailable != nil {
maxUnavailableIntStr := intstr.FromInt(*maxUnavailable)
rollingUpdate = apps.RollingUpdateStatefulSetStrategy{
MaxUnavailable: &maxUnavailableIntStr,
}
}
return &apps.StatefulSet{
Spec: apps.StatefulSetSpec{
UpdateStrategy: apps.StatefulSetUpdateStrategy{
Type: apps.RollingUpdateStatefulSetStrategyType,
RollingUpdate: &rollingUpdate,
},
},
}
}
func getMaxUnavailable(maxUnavailable int) *int {
return &maxUnavailable
}
// TestDropStatefulSetDisabledFields tests if the drop functionality is working fine or not
func TestDropStatefulSetDisabledFields(t *testing.T) {
testCases := []struct {
name string
enableMinReadySeconds bool
enableMaxUnavailable bool
ss *apps.StatefulSet
oldSS *apps.StatefulSet
expectedSS *apps.StatefulSet
@@ -418,21 +442,57 @@ func TestDropStatefulSetDisabledFields(t *testing.T) {
oldSS: generateStatefulSetWithMinReadySeconds(0),
expectedSS: generateStatefulSetWithMinReadySeconds(10),
},
{
name: "MaxUnavailable not enabled, field not used",
enableMaxUnavailable: false,
ss: makeStatefulSetWithMaxUnavailable(nil),
oldSS: nil,
expectedSS: makeStatefulSetWithMaxUnavailable(nil),
},
{
name: "MaxUnavailable not enabled, field used in new, not in old",
enableMaxUnavailable: false,
ss: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(3)),
oldSS: nil,
expectedSS: makeStatefulSetWithMaxUnavailable(nil),
},
{
name: "MaxUnavailable not enabled, field used in old and new",
enableMaxUnavailable: false,
ss: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(3)),
oldSS: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(3)),
expectedSS: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(3)),
},
{
name: "MaxUnavailable enabled, field used in new only",
enableMaxUnavailable: true,
ss: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(3)),
oldSS: nil,
expectedSS: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(3)),
},
{
name: "MaxUnavailable enabled, field used in both old and new",
enableMaxUnavailable: true,
ss: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(1)),
oldSS: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(3)),
expectedSS: makeStatefulSetWithMaxUnavailable(getMaxUnavailable(1)),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MaxUnavailableStatefulSet, tc.enableMaxUnavailable)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StatefulSetMinReadySeconds, tc.enableMinReadySeconds)()
old := tc.oldSS.DeepCopy()
dropStatefulSetDisabledFields(tc.ss, tc.oldSS)
// old obj should never be changed
if !reflect.DeepEqual(tc.oldSS, old) {
t.Fatalf("old ds changed: %v", diff.ObjectReflectDiff(tc.oldSS, old))
if diff := cmp.Diff(tc.oldSS, old); diff != "" {
t.Fatalf("%v: old statefulSet changed: %v", tc.name, diff)
}
if !reflect.DeepEqual(tc.ss, tc.expectedSS) {
t.Fatalf("unexpected ds spec: %v", diff.ObjectReflectDiff(tc.expectedSS, tc.ss))
if diff := cmp.Diff(tc.expectedSS, tc.ss); diff != "" {
t.Fatalf("%v: unexpected statefulSet spec: %v, want %v, got %v", tc.name, diff, tc.expectedSS, tc.ss)
}
})
}