API: maxUnavailable for StatefulSet
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: ¬TheDefaultPartition,
|
||||
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: ¬TheDefaultPartition,
|
||||
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)
|
||||
|
||||
3
pkg/apis/apps/v1/zz_generated.conversion.go
generated
3
pkg/apis/apps/v1/zz_generated.conversion.go
generated
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
pkg/apis/apps/v1beta1/zz_generated.conversion.go
generated
3
pkg/apis/apps/v1beta1/zz_generated.conversion.go
generated
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: ¬TheDefaultPartition,
|
||||
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: ¬TheDefaultPartition,
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
pkg/apis/apps/v1beta2/zz_generated.conversion.go
generated
3
pkg/apis/apps/v1beta2/zz_generated.conversion.go
generated
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
8
pkg/apis/apps/zz_generated.deepcopy.go
generated
8
pkg/apis/apps/zz_generated.deepcopy.go
generated
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
30
pkg/generated/openapi/zz_generated.openapi.go
generated
30
pkg/generated/openapi/zz_generated.openapi.go
generated
@@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user