Adding implementation of KEP-3335, StatefulSetSlice

This commit is contained in:
Peter Schuurman
2022-09-26 18:34:03 -07:00
parent 2b7b5245ea
commit 7b3d77a41a
37 changed files with 1804 additions and 507 deletions

View File

@@ -143,6 +143,17 @@ type StatefulSetPersistentVolumeClaimRetentionPolicy struct {
WhenScaled PersistentVolumeClaimRetentionPolicyType
}
// StatefulSetOrdinals describes the policy used for replica ordinal assignment
// in this StatefulSet.
type StatefulSetOrdinals struct {
// Start is the number representing the first index that is used to represent
// replica ordinals. Defaults to 0.
// If set, replica ordinals will be numbered
// [.spec.ordinals.start, .spec.ordinals.start - .spec.replicas).
// +optional
Start int32
}
// A StatefulSetSpec is the specification of a StatefulSet.
type StatefulSetSpec struct {
// Replicas is the desired number of replicas of the given Template.
@@ -215,6 +226,14 @@ type StatefulSetSpec struct {
// StatefulSetAutoDeletePVC feature gate to be enabled, which is alpha.
// +optional
PersistentVolumeClaimRetentionPolicy *StatefulSetPersistentVolumeClaimRetentionPolicy
// Ordinals controls how the stateful set creates pod and persistent volume
// claim names.
// The default behavior assigns a number starting with zero and incremented by
// one for each additional replica requested. This requires the
// StatefulSetSlice feature gate to be enabled, which is alpha.
// +optional
Ordinals *StatefulSetOrdinals
}
// StatefulSetStatus represents the current state of a StatefulSet.

View File

@@ -1206,6 +1206,7 @@ func autoConvert_v1_StatefulSetSpec_To_apps_StatefulSetSpec(in *v1.StatefulSetSp
out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
out.MinReadySeconds = in.MinReadySeconds
out.PersistentVolumeClaimRetentionPolicy = (*apps.StatefulSetPersistentVolumeClaimRetentionPolicy)(unsafe.Pointer(in.PersistentVolumeClaimRetentionPolicy))
// WARNING: in.Ordinals requires manual conversion: does not exist in peer-type
return nil
}
@@ -1226,6 +1227,7 @@ func autoConvert_apps_StatefulSetSpec_To_v1_StatefulSetSpec(in *apps.StatefulSet
out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
out.MinReadySeconds = in.MinReadySeconds
out.PersistentVolumeClaimRetentionPolicy = (*v1.StatefulSetPersistentVolumeClaimRetentionPolicy)(unsafe.Pointer(in.PersistentVolumeClaimRetentionPolicy))
// WARNING: in.ReplicaStartOrdinal requires manual conversion: does not exist in peer-type
return nil
}

View File

@@ -870,6 +870,7 @@ func autoConvert_v1beta1_StatefulSetSpec_To_apps_StatefulSetSpec(in *v1beta1.Sta
out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
out.MinReadySeconds = in.MinReadySeconds
out.PersistentVolumeClaimRetentionPolicy = (*apps.StatefulSetPersistentVolumeClaimRetentionPolicy)(unsafe.Pointer(in.PersistentVolumeClaimRetentionPolicy))
// WARNING: in.Ordinals requires manual conversion: does not exist in peer-type
return nil
}
@@ -890,6 +891,7 @@ func autoConvert_apps_StatefulSetSpec_To_v1beta1_StatefulSetSpec(in *apps.Statef
out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
out.MinReadySeconds = in.MinReadySeconds
out.PersistentVolumeClaimRetentionPolicy = (*v1beta1.StatefulSetPersistentVolumeClaimRetentionPolicy)(unsafe.Pointer(in.PersistentVolumeClaimRetentionPolicy))
// WARNING: in.ReplicaStartOrdinal requires manual conversion: does not exist in peer-type
return nil
}

View File

@@ -1302,6 +1302,7 @@ func autoConvert_v1beta2_StatefulSetSpec_To_apps_StatefulSetSpec(in *v1beta2.Sta
out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
out.MinReadySeconds = in.MinReadySeconds
out.PersistentVolumeClaimRetentionPolicy = (*apps.StatefulSetPersistentVolumeClaimRetentionPolicy)(unsafe.Pointer(in.PersistentVolumeClaimRetentionPolicy))
// WARNING: in.Ordinals requires manual conversion: does not exist in peer-type
return nil
}
@@ -1322,6 +1323,7 @@ func autoConvert_apps_StatefulSetSpec_To_v1beta2_StatefulSetSpec(in *apps.Statef
out.RevisionHistoryLimit = (*int32)(unsafe.Pointer(in.RevisionHistoryLimit))
out.MinReadySeconds = in.MinReadySeconds
out.PersistentVolumeClaimRetentionPolicy = (*v1beta2.StatefulSetPersistentVolumeClaimRetentionPolicy)(unsafe.Pointer(in.PersistentVolumeClaimRetentionPolicy))
// WARNING: in.ReplicaStartOrdinal requires manual conversion: does not exist in peer-type
return nil
}

View File

@@ -28,9 +28,11 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/apis/apps"
api "k8s.io/kubernetes/pkg/apis/core"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/features"
)
// ValidateStatefulSetName can be used to check whether the given StatefulSet name is valid.
@@ -128,6 +130,12 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...)
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...)
if utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetSlice) {
if spec.Ordinals != nil {
replicaStartOrdinal := spec.Ordinals.Start
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(replicaStartOrdinal), fldPath.Child("ordinals.start"))...)
}
}
if spec.Selector == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
@@ -177,10 +185,17 @@ func ValidateStatefulSetUpdate(statefulSet, oldStatefulSet *apps.StatefulSet, op
newStatefulSetClone.Spec.Template = oldStatefulSet.Spec.Template // +k8s:verify-mutation:reason=clone
newStatefulSetClone.Spec.UpdateStrategy = oldStatefulSet.Spec.UpdateStrategy // +k8s:verify-mutation:reason=clone
newStatefulSetClone.Spec.MinReadySeconds = oldStatefulSet.Spec.MinReadySeconds // +k8s:verify-mutation:reason=clone
if utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetSlice) {
newStatefulSetClone.Spec.Ordinals = oldStatefulSet.Spec.Ordinals // +k8s:verify-mutation:reason=clone
}
newStatefulSetClone.Spec.PersistentVolumeClaimRetentionPolicy = oldStatefulSet.Spec.PersistentVolumeClaimRetentionPolicy // +k8s:verify-mutation:reason=clone
if !apiequality.Semantic.DeepEqual(newStatefulSetClone.Spec, oldStatefulSet.Spec) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "updates to statefulset spec for fields other than 'replicas', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden"))
if utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetSlice) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden"))
} else {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "updates to statefulset spec for fields other than 'replicas', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden"))
}
}
return allErrs

View File

@@ -86,6 +86,8 @@ func TestValidateStatefulSet(t *testing.T) {
const enableStatefulSetAutoDeletePVC = "[enable StatefulSetAutoDeletePVC]"
const enableStatefulSetSlice = "[enable StatefulSetSlice]"
type testCase struct {
name string
set apps.StatefulSet
@@ -193,6 +195,20 @@ func TestValidateStatefulSet(t *testing.T) {
},
},
},
{
name: "ordinals.start positive value " + enableStatefulSetSlice,
set: apps.StatefulSet{
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
Spec: apps.StatefulSetSpec{
PodManagementPolicy: apps.ParallelPodManagement,
UpdateStrategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType},
Selector: &metav1.LabelSelector{MatchLabels: validLabels},
Template: validPodTemplate.Template,
Replicas: 3,
Ordinals: &apps.StatefulSetOrdinals{Start: 2},
},
},
},
}
errorCases := []testCase{
@@ -635,6 +651,23 @@ func TestValidateStatefulSet(t *testing.T) {
field.Invalid(field.NewPath("spec", "updateStrategy", "rollingUpdate", "maxUnavailable"), nil, ""),
},
},
{
name: "invalid ordinals.start " + enableStatefulSetSlice,
set: apps.StatefulSet{
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
Spec: apps.StatefulSetSpec{
PodManagementPolicy: apps.ParallelPodManagement,
UpdateStrategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType},
Selector: &metav1.LabelSelector{MatchLabels: validLabels},
Template: validPodTemplate.Template,
Replicas: 3,
Ordinals: &apps.StatefulSetOrdinals{Start: -2},
},
},
errs: field.ErrorList{
field.Invalid(field.NewPath("spec", "ordinals.start"), nil, ""),
},
},
}
cmpOpts := []cmp.Option{cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail"), cmpopts.SortSlices(func(a, b *field.Error) bool { return a.Error() < b.Error() })}
@@ -651,6 +684,9 @@ func TestValidateStatefulSet(t *testing.T) {
if strings.Contains(name, enableStatefulSetAutoDeletePVC) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StatefulSetAutoDeletePVC, true)()
}
if strings.Contains(name, enableStatefulSetSlice) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StatefulSetSlice, true)()
}
errs := ValidateStatefulSet(&testCase.set, pod.GetValidationOptionsFromPodTemplate(&testCase.set.Spec.Template, nil))
wantErrs := testCase.errs

View File

@@ -303,52 +303,53 @@ func (ssc *defaultStatefulSetControl) updateStatefulSet(
*status.CollisionCount = collisionCount
replicaCount := int(*set.Spec.Replicas)
// slice that will contain all Pods such that 0 <= getOrdinal(pod) < set.Spec.Replicas
// slice that will contain all Pods such that getStartOrdinal(set) <= getOrdinal(pod) <= getEndOrdinal(set)
replicas := make([]*v1.Pod, replicaCount)
// slice that will contain all Pods such that set.Spec.Replicas <= getOrdinal(pod)
// slice that will contain all Pods such that getOrdinal(pod) < getStartOrdinal(set) OR getOrdinal(pod) > getEndOrdinal(set)
condemned := make([]*v1.Pod, 0, len(pods))
unhealthy := 0
var firstUnhealthyPod *v1.Pod
// First we partition pods into two lists valid replicas and condemned Pods
for i := range pods {
for _, pod := range pods {
status.Replicas++
// count the number of running and ready replicas
if isRunningAndReady(pods[i]) {
if isRunningAndReady(pod) {
status.ReadyReplicas++
// count the number of running and available replicas
if isRunningAndAvailable(pods[i], set.Spec.MinReadySeconds) {
if isRunningAndAvailable(pod, set.Spec.MinReadySeconds) {
status.AvailableReplicas++
}
}
// count the number of current and update replicas
if isCreated(pods[i]) && !isTerminating(pods[i]) {
if getPodRevision(pods[i]) == currentRevision.Name {
if isCreated(pod) && !isTerminating(pod) {
if getPodRevision(pod) == currentRevision.Name {
status.CurrentReplicas++
}
if getPodRevision(pods[i]) == updateRevision.Name {
if getPodRevision(pod) == updateRevision.Name {
status.UpdatedReplicas++
}
}
if ord := getOrdinal(pods[i]); 0 <= ord && ord < replicaCount {
if podInOrdinalRange(pod, set) {
// if the ordinal of the pod is within the range of the current number of replicas,
// insert it at the indirection of its ordinal
replicas[ord] = pods[i]
} else if ord >= replicaCount {
// if the ordinal is greater than the number of replicas add it to the condemned list
condemned = append(condemned, pods[i])
replicas[getOrdinal(pod)-getStartOrdinal(set)] = pod
} else if getOrdinal(pod) >= 0 {
// if the ordinal is valid, but not within the range add it to the condemned list
condemned = append(condemned, pod)
}
// If the ordinal could not be parsed (ord < 0), ignore the Pod.
}
// for any empty indices in the sequence [0,set.Spec.Replicas) create a new Pod at the correct revision
for ord := 0; ord < replicaCount; ord++ {
if replicas[ord] == nil {
replicas[ord] = newVersionedStatefulSetPod(
for ord := getStartOrdinal(set); ord <= getEndOrdinal(set); ord++ {
replicaIdx := ord - getStartOrdinal(set)
if replicas[replicaIdx] == nil {
replicas[replicaIdx] = newVersionedStatefulSetPod(
currentSet,
updateSet,
currentRevision.Name,
@@ -409,12 +410,13 @@ func (ssc *defaultStatefulSetControl) updateStatefulSet(
status.UpdatedReplicas--
}
status.Replicas--
replicaOrd := i + getStartOrdinal(set)
replicas[i] = newVersionedStatefulSetPod(
currentSet,
updateSet,
currentRevision.Name,
updateRevision.Name,
i)
replicaOrd)
}
// If we find a Pod that has not been created we create the Pod
if !isCreated(replicas[i]) {
@@ -501,7 +503,7 @@ func (ssc *defaultStatefulSetControl) updateStatefulSet(
// At this point, all of the current Replicas are Running, Ready and Available, we can consider termination.
// We will wait for all predecessors to be Running and Ready prior to attempting a deletion.
// We will terminate Pods in a monotonically decreasing order over [len(pods),set.Spec.Replicas).
// We will terminate Pods in a monotonically decreasing order.
// Note that we do not resurrect Pods in this interval. Also note that scaling will take precedence over
// updates.
for target := len(condemned) - 1; target >= 0; target-- {
@@ -614,7 +616,7 @@ func updateStatefulSetAfterInvariantEstablished(
}
}
// Collect all targets in the range between the 0 and Spec.Replicas. Count any targets in that range
// Collect all targets in the range between getStartOrdinal(set) and getEndOrdinal(set). Count any targets in that range
// that are unhealthy i.e. terminated or not running and ready as unavailable). Select the
// (MaxUnavailable - Unavailable) Pods, in order with respect to their ordinal for termination. Delete
// those pods and count the successful deletions. Update the status with the correct number of deletions.

View File

@@ -559,6 +559,74 @@ func PodRecreateDeleteFailure(t *testing.T, set *apps.StatefulSet, invariants in
}
}
func emptyInvaraints(set *apps.StatefulSet, om *fakeObjectManager) error {
return nil
}
func TestStatefulSetControlWithStartOrdinal(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StatefulSetSlice, true)()
simpleSetFn := func() *apps.StatefulSet {
statefulSet := newStatefulSet(3)
statefulSet.Spec.Ordinals = &apps.StatefulSetOrdinals{Start: int32(2)}
return statefulSet
}
testCases := []struct {
fn func(*testing.T, *apps.StatefulSet, invariantFunc)
obj func() *apps.StatefulSet
}{
{CreatesPodsWithStartOrdinal, simpleSetFn},
}
for _, testCase := range testCases {
testObj := testCase.obj
testFn := testCase.fn
set := testObj()
testFn(t, set, emptyInvaraints)
}
}
func CreatesPodsWithStartOrdinal(t *testing.T, set *apps.StatefulSet, invariants invariantFunc) {
client := fake.NewSimpleClientset(set)
om, _, ssc := setupController(client)
if err := scaleUpStatefulSetControl(set, ssc, om, invariants); err != nil {
t.Errorf("Failed to turn up StatefulSet : %s", err)
}
var err error
set, err = om.setsLister.StatefulSets(set.Namespace).Get(set.Name)
if err != nil {
t.Fatalf("Error getting updated StatefulSet: %v", err)
}
if set.Status.Replicas != 3 {
t.Error("Failed to scale statefulset to 3 replicas")
}
if set.Status.ReadyReplicas != 3 {
t.Error("Failed to set ReadyReplicas correctly")
}
if set.Status.UpdatedReplicas != 3 {
t.Error("Failed to set UpdatedReplicas correctly")
}
selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector)
if err != nil {
t.Error(err)
}
pods, err := om.podsLister.Pods(set.Namespace).List(selector)
if err != nil {
t.Error(err)
}
sort.Sort(ascendingOrdinal(pods))
for i, pod := range pods {
expectedOrdinal := 2 + i
actualPodOrdinal := getOrdinal(pod)
if actualPodOrdinal != expectedOrdinal {
t.Errorf("Expected pod ordinal %d. Got %d", expectedOrdinal, actualPodOrdinal)
}
}
}
func TestStatefulSetControlScaleDownDeleteError(t *testing.T) {
runTestOverPVCRetentionPolicies(
t, "", func(t *testing.T, policy *apps.StatefulSetPersistentVolumeClaimRetentionPolicy) {
@@ -2299,6 +2367,16 @@ func (om *fakeObjectManager) SetDeleteStatefulPodError(err error, after int) {
om.deletePodTracker.after = after
}
func findPodByOrdinal(pods []*v1.Pod, ordinal int) *v1.Pod {
for _, pod := range pods {
if getOrdinal(pod) == ordinal {
return pod.DeepCopy()
}
}
return nil
}
func (om *fakeObjectManager) setPodPending(set *apps.StatefulSet, ordinal int) ([]*v1.Pod, error) {
selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector)
if err != nil {
@@ -2308,11 +2386,10 @@ func (om *fakeObjectManager) setPodPending(set *apps.StatefulSet, ordinal int) (
if err != nil {
return nil, err
}
if 0 > ordinal || ordinal >= len(pods) {
return nil, fmt.Errorf("ordinal %d out of range [0,%d)", ordinal, len(pods))
pod := findPodByOrdinal(pods, ordinal)
if pod == nil {
return nil, fmt.Errorf("setPodPending: pod ordinal %d not found", ordinal)
}
sort.Sort(ascendingOrdinal(pods))
pod := pods[ordinal].DeepCopy()
pod.Status.Phase = v1.PodPending
fakeResourceVersion(pod)
om.podsIndexer.Update(pod)
@@ -2328,11 +2405,10 @@ func (om *fakeObjectManager) setPodRunning(set *apps.StatefulSet, ordinal int) (
if err != nil {
return nil, err
}
if 0 > ordinal || ordinal >= len(pods) {
return nil, fmt.Errorf("ordinal %d out of range [0,%d)", ordinal, len(pods))
pod := findPodByOrdinal(pods, ordinal)
if pod == nil {
return nil, fmt.Errorf("setPodRunning: pod ordinal %d not found", ordinal)
}
sort.Sort(ascendingOrdinal(pods))
pod := pods[ordinal].DeepCopy()
pod.Status.Phase = v1.PodRunning
fakeResourceVersion(pod)
om.podsIndexer.Update(pod)
@@ -2348,11 +2424,10 @@ func (om *fakeObjectManager) setPodReady(set *apps.StatefulSet, ordinal int) ([]
if err != nil {
return nil, err
}
if 0 > ordinal || ordinal >= len(pods) {
return nil, fmt.Errorf("ordinal %d out of range [0,%d)", ordinal, len(pods))
pod := findPodByOrdinal(pods, ordinal)
if pod == nil {
return nil, fmt.Errorf("setPodReady: pod ordinal %d not found", ordinal)
}
sort.Sort(ascendingOrdinal(pods))
pod := pods[ordinal].DeepCopy()
condition := v1.PodCondition{Type: v1.PodReady, Status: v1.ConditionTrue}
podutil.UpdatePodCondition(&pod.Status, &condition)
fakeResourceVersion(pod)
@@ -2369,11 +2444,10 @@ func (om *fakeObjectManager) setPodAvailable(set *apps.StatefulSet, ordinal int,
if err != nil {
return nil, err
}
if 0 > ordinal || ordinal >= len(pods) {
return nil, fmt.Errorf("ordinal %d out of range [0,%d)", ordinal, len(pods))
pod := findPodByOrdinal(pods, ordinal)
if pod == nil {
return nil, fmt.Errorf("setPodAvailable: pod ordinal %d not found", ordinal)
}
sort.Sort(ascendingOrdinal(pods))
pod := pods[ordinal].DeepCopy()
condition := v1.PodCondition{Type: v1.PodReady, Status: v1.ConditionTrue, LastTransitionTime: metav1.Time{Time: lastTransitionTime}}
_, existingCondition := podutil.GetPodCondition(&pod.Status, condition.Type)
if existingCondition != nil {
@@ -2467,28 +2541,28 @@ func assertMonotonicInvariants(set *apps.StatefulSet, om *fakeObjectManager) err
return err
}
sort.Sort(ascendingOrdinal(pods))
for ord := 0; ord < len(pods); ord++ {
if ord > 0 && isRunningAndReady(pods[ord]) && !isRunningAndReady(pods[ord-1]) {
return fmt.Errorf("successor %s is Running and Ready while %s is not", pods[ord].Name, pods[ord-1].Name)
for idx := 0; idx < len(pods); idx++ {
if idx > 0 && isRunningAndReady(pods[idx]) && !isRunningAndReady(pods[idx-1]) {
return fmt.Errorf("successor %s is Running and Ready while %s is not", pods[idx].Name, pods[idx-1].Name)
}
if getOrdinal(pods[ord]) != ord {
return fmt.Errorf("pods %s deployed in the wrong order %d", pods[ord].Name, ord)
if ord := idx + getStartOrdinal(set); getOrdinal(pods[idx]) != ord {
return fmt.Errorf("pods %s deployed in the wrong order %d", pods[idx].Name, ord)
}
if !storageMatches(set, pods[ord]) {
return fmt.Errorf("pods %s does not match the storage specification of StatefulSet %s ", pods[ord].Name, set.Name)
if !storageMatches(set, pods[idx]) {
return fmt.Errorf("pods %s does not match the storage specification of StatefulSet %s ", pods[idx].Name, set.Name)
}
for _, claim := range getPersistentVolumeClaims(set, pods[ord]) {
for _, claim := range getPersistentVolumeClaims(set, pods[idx]) {
claim, _ := om.claimsLister.PersistentVolumeClaims(set.Namespace).Get(claim.Name)
if err := checkClaimInvarients(set, pods[ord], claim, ord); err != nil {
if err := checkClaimInvarients(set, pods[idx], claim); err != nil {
return err
}
}
if !identityMatches(set, pods[ord]) {
return fmt.Errorf("pods %s does not match the identity specification of StatefulSet %s ", pods[ord].Name, set.Name)
if !identityMatches(set, pods[idx]) {
return fmt.Errorf("pods %s does not match the identity specification of StatefulSet %s ", pods[idx].Name, set.Name)
}
}
return nil
@@ -2504,24 +2578,24 @@ func assertBurstInvariants(set *apps.StatefulSet, om *fakeObjectManager) error {
return err
}
sort.Sort(ascendingOrdinal(pods))
for ord := 0; ord < len(pods); ord++ {
if !storageMatches(set, pods[ord]) {
return fmt.Errorf("pods %s does not match the storage specification of StatefulSet %s ", pods[ord].Name, set.Name)
for _, pod := range pods {
if !storageMatches(set, pod) {
return fmt.Errorf("pods %s does not match the storage specification of StatefulSet %s ", pod.Name, set.Name)
}
for _, claim := range getPersistentVolumeClaims(set, pods[ord]) {
for _, claim := range getPersistentVolumeClaims(set, pod) {
claim, err := om.claimsLister.PersistentVolumeClaims(set.Namespace).Get(claim.Name)
if err != nil {
return err
}
if err := checkClaimInvarients(set, pods[ord], claim, ord); err != nil {
if err := checkClaimInvarients(set, pod, claim); err != nil {
return err
}
}
if !identityMatches(set, pods[ord]) {
if !identityMatches(set, pod) {
return fmt.Errorf("pods %s does not match the identity specification of StatefulSet %s ",
pods[ord].Name,
pod.Name,
set.Name)
}
}
@@ -2538,24 +2612,24 @@ func assertUpdateInvariants(set *apps.StatefulSet, om *fakeObjectManager) error
return err
}
sort.Sort(ascendingOrdinal(pods))
for ord := 0; ord < len(pods); ord++ {
for _, pod := range pods {
if !storageMatches(set, pods[ord]) {
return fmt.Errorf("pod %s does not match the storage specification of StatefulSet %s ", pods[ord].Name, set.Name)
if !storageMatches(set, pod) {
return fmt.Errorf("pod %s does not match the storage specification of StatefulSet %s ", pod.Name, set.Name)
}
for _, claim := range getPersistentVolumeClaims(set, pods[ord]) {
for _, claim := range getPersistentVolumeClaims(set, pod) {
claim, err := om.claimsLister.PersistentVolumeClaims(set.Namespace).Get(claim.Name)
if err != nil {
return err
}
if err := checkClaimInvarients(set, pods[ord], claim, ord); err != nil {
if err := checkClaimInvarients(set, pod, claim); err != nil {
return err
}
}
if !identityMatches(set, pods[ord]) {
return fmt.Errorf("pod %s does not match the identity specification of StatefulSet %s ", pods[ord].Name, set.Name)
if !identityMatches(set, pod) {
return fmt.Errorf("pod %s does not match the identity specification of StatefulSet %s ", pod.Name, set.Name)
}
}
if set.Spec.UpdateStrategy.Type == apps.OnDeleteStatefulSetStrategyType {
@@ -2576,7 +2650,7 @@ func assertUpdateInvariants(set *apps.StatefulSet, om *fakeObjectManager) error
return nil
}
func checkClaimInvarients(set *apps.StatefulSet, pod *v1.Pod, claim *v1.PersistentVolumeClaim, ordinal int) error {
func checkClaimInvarients(set *apps.StatefulSet, pod *v1.Pod, claim *v1.PersistentVolumeClaim) error {
policy := apps.StatefulSetPersistentVolumeClaimRetentionPolicy{
WhenScaled: apps.RetainPersistentVolumeClaimRetentionPolicyType,
WhenDeleted: apps.RetainPersistentVolumeClaimRetentionPolicyType,
@@ -2618,14 +2692,14 @@ func checkClaimInvarients(set *apps.StatefulSet, pod *v1.Pod, claim *v1.Persiste
if hasOwnerRef(claim, set) {
return fmt.Errorf("claim %s has unexpected owner ref on %s for scaledown only", claim.Name, set.Name)
}
if ordinal >= int(*set.Spec.Replicas) && !hasOwnerRef(claim, pod) {
if !podInOrdinalRange(pod, set) && !hasOwnerRef(claim, pod) {
return fmt.Errorf("claim %s does not have owner ref on condemned pod %s for scaledown delete", claim.Name, pod.Name)
}
if ordinal < int(*set.Spec.Replicas) && hasOwnerRef(claim, pod) {
return fmt.Errorf("claim %s has unexpected owner ref on condemned pod %s for scaledown delete", claim.Name, pod.Name)
if podInOrdinalRange(pod, set) && hasOwnerRef(claim, pod) {
return fmt.Errorf("claim %s has unexpected owner ref on condemned pod %s for scaledown delete. ordinal: %d, [%d,%d]", claim.Name, pod.Name, getOrdinal(pod), getStartOrdinal(set), getEndOrdinal(set))
}
case policy.WhenScaled == delete && policy.WhenDeleted == delete:
if ordinal >= int(*set.Spec.Replicas) {
if !podInOrdinalRange(pod, set) {
if !hasOwnerRef(claim, pod) || hasOwnerRef(claim, set) {
return fmt.Errorf("condemned claim %s has bad owner refs: %v", claim.Name, claim.GetOwnerReferences())
}
@@ -2666,9 +2740,9 @@ func scaleUpStatefulSetControl(set *apps.StatefulSet,
sort.Sort(ascendingOrdinal(pods))
// ensure all pods are valid (have a phase)
for ord, pod := range pods {
for _, pod := range pods {
if pod.Status.Phase == "" {
if pods, err = om.setPodPending(set, ord); err != nil {
if pods, err = om.setPodPending(set, getOrdinal(pod)); err != nil {
return err
}
break
@@ -2677,15 +2751,15 @@ func scaleUpStatefulSetControl(set *apps.StatefulSet,
// select one of the pods and move it forward in status
if len(pods) > 0 {
ord := int(rand.Int63n(int64(len(pods))))
pod := pods[ord]
idx := int(rand.Int63n(int64(len(pods))))
pod := pods[idx]
switch pod.Status.Phase {
case v1.PodPending:
if pods, err = om.setPodRunning(set, ord); err != nil {
if pods, err = om.setPodRunning(set, getOrdinal(pod)); err != nil {
return err
}
case v1.PodRunning:
if pods, err = om.setPodReady(set, ord); err != nil {
if pods, err = om.setPodReady(set, getOrdinal(pod)); err != nil {
return err
}
default:
@@ -2720,7 +2794,7 @@ func scaleDownStatefulSetControl(set *apps.StatefulSet, ssc StatefulSetControlIn
return err
}
sort.Sort(ascendingOrdinal(pods))
if ordinal := len(pods) - 1; ordinal >= 0 {
if idx := len(pods) - 1; idx >= 0 {
if _, err := ssc.UpdateStatefulSet(context.TODO(), set, pods); err != nil {
return err
}
@@ -2728,7 +2802,7 @@ func scaleDownStatefulSetControl(set *apps.StatefulSet, ssc StatefulSetControlIn
if err != nil {
return err
}
if pods, err = om.addTerminatingPod(set, ordinal); err != nil {
if pods, err = om.addTerminatingPod(set, getOrdinal(pods[idx])); err != nil {
return err
}
if _, err = ssc.UpdateStatefulSet(context.TODO(), set, pods); err != nil {
@@ -2860,9 +2934,9 @@ func updateStatefulSetControl(set *apps.StatefulSet,
}
sort.Sort(ascendingOrdinal(pods))
initialized := false
for ord, pod := range pods {
for _, pod := range pods {
if pod.Status.Phase == "" {
if pods, err = om.setPodPending(set, ord); err != nil {
if pods, err = om.setPodPending(set, getOrdinal(pod)); err != nil {
return err
}
break
@@ -2873,15 +2947,15 @@ func updateStatefulSetControl(set *apps.StatefulSet,
}
if len(pods) > 0 {
ord := int(rand.Int63n(int64(len(pods))))
pod := pods[ord]
idx := int(rand.Int63n(int64(len(pods))))
pod := pods[idx]
switch pod.Status.Phase {
case v1.PodPending:
if pods, err = om.setPodRunning(set, ord); err != nil {
if pods, err = om.setPodRunning(set, getOrdinal(pod)); err != nil {
return err
}
case v1.PodRunning:
if pods, err = om.setPodReady(set, ord); err != nil {
if pods, err = om.setPodReady(set, getOrdinal(pod)); err != nil {
return err
}
default:

View File

@@ -29,11 +29,13 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/strategicpatch"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/controller/history"
"k8s.io/kubernetes/pkg/features"
)
var patchCodec = scheme.Codecs.LegacyCodec(apps.SchemeGroupVersion)
@@ -85,6 +87,29 @@ func getOrdinal(pod *v1.Pod) int {
return ordinal
}
// getStartOrdinal gets the first possible ordinal (inclusive).
// Returns spec.ordinals.start if spec.ordinals is set, otherwise returns 0.
func getStartOrdinal(set *apps.StatefulSet) int {
if utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetSlice) {
if set.Spec.Ordinals != nil {
return int(set.Spec.Ordinals.Start)
}
}
return 0
}
// getEndOrdinal gets the last possible ordinal (inclusive).
func getEndOrdinal(set *apps.StatefulSet) int {
return getStartOrdinal(set) + int(*set.Spec.Replicas) - 1
}
// podInOrdinalRange returns true if the pod ordinal is within the allowed
// range of ordinals that this StatefulSet is set to control.
func podInOrdinalRange(pod *v1.Pod, set *apps.StatefulSet) bool {
ordinal := getOrdinal(pod)
return ordinal >= getStartOrdinal(set) && ordinal <= getEndOrdinal(set)
}
// getPodName gets the name of set's child Pod with an ordinal index of ordinal
func getPodName(set *apps.StatefulSet, ordinal int) string {
return fmt.Sprintf("%s-%d", set.Name, ordinal)
@@ -170,12 +195,12 @@ func claimOwnerMatchesSetAndPod(claim *v1.PersistentVolumeClaim, set *apps.State
if hasOwnerRef(claim, set) {
return false
}
podScaledDown := getOrdinal(pod) >= int(*set.Spec.Replicas)
podScaledDown := !podInOrdinalRange(pod, set)
if podScaledDown != hasOwnerRef(claim, pod) {
return false
}
case policy.WhenScaled == delete && policy.WhenDeleted == delete:
podScaledDown := getOrdinal(pod) >= int(*set.Spec.Replicas)
podScaledDown := !podInOrdinalRange(pod, set)
// If a pod is scaled down, there should be no set ref and a pod ref;
// if the pod is not scaled down it's the other way around.
if podScaledDown == hasOwnerRef(claim, set) {
@@ -226,7 +251,7 @@ func updateClaimOwnerRefForSetAndPod(claim *v1.PersistentVolumeClaim, set *apps.
needsUpdate = removeOwnerRef(claim, pod) || needsUpdate
case policy.WhenScaled == delete && policy.WhenDeleted == retain:
needsUpdate = removeOwnerRef(claim, set) || needsUpdate
podScaledDown := getOrdinal(pod) >= int(*set.Spec.Replicas)
podScaledDown := !podInOrdinalRange(pod, set)
if podScaledDown {
needsUpdate = setOwnerRef(claim, pod, &podMeta) || needsUpdate
}
@@ -234,7 +259,7 @@ func updateClaimOwnerRefForSetAndPod(claim *v1.PersistentVolumeClaim, set *apps.
needsUpdate = removeOwnerRef(claim, pod) || needsUpdate
}
case policy.WhenScaled == delete && policy.WhenDeleted == delete:
podScaledDown := getOrdinal(pod) >= int(*set.Spec.Replicas)
podScaledDown := !podInOrdinalRange(pod, set)
if podScaledDown {
needsUpdate = removeOwnerRef(claim, set) || needsUpdate
needsUpdate = setOwnerRef(claim, pod, &podMeta) || needsUpdate
@@ -440,8 +465,8 @@ func newStatefulSetPod(set *apps.StatefulSet, ordinal int) *v1.Pod {
// returned error is nil, the returned Pod is valid.
func newVersionedStatefulSetPod(currentSet, updateSet *apps.StatefulSet, currentRevision, updateRevision string, ordinal int) *v1.Pod {
if currentSet.Spec.UpdateStrategy.Type == apps.RollingUpdateStatefulSetStrategyType &&
(currentSet.Spec.UpdateStrategy.RollingUpdate == nil && ordinal < int(currentSet.Status.CurrentReplicas)) ||
(currentSet.Spec.UpdateStrategy.RollingUpdate != nil && ordinal < int(*currentSet.Spec.UpdateStrategy.RollingUpdate.Partition)) {
(currentSet.Spec.UpdateStrategy.RollingUpdate == nil && ordinal < (getStartOrdinal(currentSet)+int(currentSet.Status.CurrentReplicas))) ||
(currentSet.Spec.UpdateStrategy.RollingUpdate != nil && ordinal < (getStartOrdinal(currentSet)+int(*currentSet.Spec.UpdateStrategy.RollingUpdate.Partition))) {
pod := newStatefulSetPod(currentSet, ordinal)
setPodRevision(pod, currentRevision)
return pod

View File

@@ -766,6 +766,12 @@ const (
// StatefulSetMinReadySeconds allows minReadySeconds to be respected by StatefulSet controller
StatefulSetMinReadySeconds featuregate.Feature = "StatefulSetMinReadySeconds"
// owner: @psch
// alpha: v1.26
//
// Enables a StatefulSet to start from an arbitrary non zero ordinal
StatefulSetSlice featuregate.Feature = "StatefulSetSlice"
// owner: @robscott
// kep: https://kep.k8s.io/2433
// alpha: v1.21
@@ -1075,6 +1081,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
StatefulSetMinReadySeconds: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.27
StatefulSetSlice: {Default: false, PreRelease: featuregate.Alpha},
TopologyAwareHints: {Default: true, PreRelease: featuregate.Beta},
TopologyManager: {Default: true, PreRelease: featuregate.Beta},

View File

@@ -100,6 +100,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/api/apps/v1.StatefulSet": schema_k8sio_api_apps_v1_StatefulSet(ref),
"k8s.io/api/apps/v1.StatefulSetCondition": schema_k8sio_api_apps_v1_StatefulSetCondition(ref),
"k8s.io/api/apps/v1.StatefulSetList": schema_k8sio_api_apps_v1_StatefulSetList(ref),
"k8s.io/api/apps/v1.StatefulSetOrdinals": schema_k8sio_api_apps_v1_StatefulSetOrdinals(ref),
"k8s.io/api/apps/v1.StatefulSetPersistentVolumeClaimRetentionPolicy": schema_k8sio_api_apps_v1_StatefulSetPersistentVolumeClaimRetentionPolicy(ref),
"k8s.io/api/apps/v1.StatefulSetSpec": schema_k8sio_api_apps_v1_StatefulSetSpec(ref),
"k8s.io/api/apps/v1.StatefulSetStatus": schema_k8sio_api_apps_v1_StatefulSetStatus(ref),
@@ -122,6 +123,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/api/apps/v1beta1.StatefulSet": schema_k8sio_api_apps_v1beta1_StatefulSet(ref),
"k8s.io/api/apps/v1beta1.StatefulSetCondition": schema_k8sio_api_apps_v1beta1_StatefulSetCondition(ref),
"k8s.io/api/apps/v1beta1.StatefulSetList": schema_k8sio_api_apps_v1beta1_StatefulSetList(ref),
"k8s.io/api/apps/v1beta1.StatefulSetOrdinals": schema_k8sio_api_apps_v1beta1_StatefulSetOrdinals(ref),
"k8s.io/api/apps/v1beta1.StatefulSetPersistentVolumeClaimRetentionPolicy": schema_k8sio_api_apps_v1beta1_StatefulSetPersistentVolumeClaimRetentionPolicy(ref),
"k8s.io/api/apps/v1beta1.StatefulSetSpec": schema_k8sio_api_apps_v1beta1_StatefulSetSpec(ref),
"k8s.io/api/apps/v1beta1.StatefulSetStatus": schema_k8sio_api_apps_v1beta1_StatefulSetStatus(ref),
@@ -154,6 +156,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/api/apps/v1beta2.StatefulSet": schema_k8sio_api_apps_v1beta2_StatefulSet(ref),
"k8s.io/api/apps/v1beta2.StatefulSetCondition": schema_k8sio_api_apps_v1beta2_StatefulSetCondition(ref),
"k8s.io/api/apps/v1beta2.StatefulSetList": schema_k8sio_api_apps_v1beta2_StatefulSetList(ref),
"k8s.io/api/apps/v1beta2.StatefulSetOrdinals": schema_k8sio_api_apps_v1beta2_StatefulSetOrdinals(ref),
"k8s.io/api/apps/v1beta2.StatefulSetPersistentVolumeClaimRetentionPolicy": schema_k8sio_api_apps_v1beta2_StatefulSetPersistentVolumeClaimRetentionPolicy(ref),
"k8s.io/api/apps/v1beta2.StatefulSetSpec": schema_k8sio_api_apps_v1beta2_StatefulSetSpec(ref),
"k8s.io/api/apps/v1beta2.StatefulSetStatus": schema_k8sio_api_apps_v1beta2_StatefulSetStatus(ref),
@@ -4857,6 +4860,27 @@ func schema_k8sio_api_apps_v1_StatefulSetList(ref common.ReferenceCallback) comm
}
}
func schema_k8sio_api_apps_v1_StatefulSetOrdinals(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "StatefulSetOrdinals describes the policy used for replica ordinal assignment in this StatefulSet.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"start": {
SchemaProps: spec.SchemaProps{
Description: "Start is the number representing the first index that is used to represent replica ordinals. Defaults to 0. If set, replica ordinals will be numbered [.spec.ordinals.start, .spec.ordinals.start - .spec.replicas).",
Default: 0,
Type: []string{"integer"},
Format: "int32",
},
},
},
},
},
}
}
func schema_k8sio_api_apps_v1_StatefulSetPersistentVolumeClaimRetentionPolicy(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -4967,12 +4991,18 @@ func schema_k8sio_api_apps_v1_StatefulSetSpec(ref common.ReferenceCallback) comm
Ref: ref("k8s.io/api/apps/v1.StatefulSetPersistentVolumeClaimRetentionPolicy"),
},
},
"ordinals": {
SchemaProps: spec.SchemaProps{
Description: "Ordinals controls how the stateful set creates pod and persistent volume claim names. The default behavior assigns a number starting with zero and incremented by one for each additional replica requested. This requires the StatefulSetSlice feature gate to be enabled, which is alpha.",
Ref: ref("k8s.io/api/apps/v1.StatefulSetOrdinals"),
},
},
},
Required: []string{"selector", "template", "serviceName"},
},
},
Dependencies: []string{
"k8s.io/api/apps/v1.StatefulSetPersistentVolumeClaimRetentionPolicy", "k8s.io/api/apps/v1.StatefulSetUpdateStrategy", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodTemplateSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
"k8s.io/api/apps/v1.StatefulSetOrdinals", "k8s.io/api/apps/v1.StatefulSetPersistentVolumeClaimRetentionPolicy", "k8s.io/api/apps/v1.StatefulSetUpdateStrategy", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodTemplateSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
}
}
@@ -5965,6 +5995,27 @@ func schema_k8sio_api_apps_v1beta1_StatefulSetList(ref common.ReferenceCallback)
}
}
func schema_k8sio_api_apps_v1beta1_StatefulSetOrdinals(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "StatefulSetOrdinals describes the policy used for replica ordinal assignment in this StatefulSet.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"start": {
SchemaProps: spec.SchemaProps{
Description: "Start is the number representing the first index that is used to represent replica ordinals. Defaults to 0. If set, replica ordinals will be numbered [.spec.ordinals.start, .spec.ordinals.start - .spec.replicas).",
Default: 0,
Type: []string{"integer"},
Format: "int32",
},
},
},
},
},
}
}
func schema_k8sio_api_apps_v1beta1_StatefulSetPersistentVolumeClaimRetentionPolicy(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -6075,12 +6126,18 @@ func schema_k8sio_api_apps_v1beta1_StatefulSetSpec(ref common.ReferenceCallback)
Ref: ref("k8s.io/api/apps/v1beta1.StatefulSetPersistentVolumeClaimRetentionPolicy"),
},
},
"ordinals": {
SchemaProps: spec.SchemaProps{
Description: "Ordinals controls how the stateful set creates pod and persistent volume claim names. The default behavior assigns a number starting with zero and incremented by one for each additional replica requested. This requires the StatefulSetSlice feature gate to be enabled, which is alpha.",
Ref: ref("k8s.io/api/apps/v1beta1.StatefulSetOrdinals"),
},
},
},
Required: []string{"template", "serviceName"},
},
},
Dependencies: []string{
"k8s.io/api/apps/v1beta1.StatefulSetPersistentVolumeClaimRetentionPolicy", "k8s.io/api/apps/v1beta1.StatefulSetUpdateStrategy", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodTemplateSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
"k8s.io/api/apps/v1beta1.StatefulSetOrdinals", "k8s.io/api/apps/v1beta1.StatefulSetPersistentVolumeClaimRetentionPolicy", "k8s.io/api/apps/v1beta1.StatefulSetUpdateStrategy", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodTemplateSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
}
}
@@ -7622,6 +7679,27 @@ func schema_k8sio_api_apps_v1beta2_StatefulSetList(ref common.ReferenceCallback)
}
}
func schema_k8sio_api_apps_v1beta2_StatefulSetOrdinals(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "StatefulSetOrdinals describes the policy used for replica ordinal assignment in this StatefulSet.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"start": {
SchemaProps: spec.SchemaProps{
Description: "Start is the number representing the first index that is used to represent replica ordinals. Defaults to 0. If set, replica ordinals will be numbered [.spec.ordinals.start, .spec.ordinals.start - .spec.replicas).",
Default: 0,
Type: []string{"integer"},
Format: "int32",
},
},
},
},
},
}
}
func schema_k8sio_api_apps_v1beta2_StatefulSetPersistentVolumeClaimRetentionPolicy(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -7732,12 +7810,18 @@ func schema_k8sio_api_apps_v1beta2_StatefulSetSpec(ref common.ReferenceCallback)
Ref: ref("k8s.io/api/apps/v1beta2.StatefulSetPersistentVolumeClaimRetentionPolicy"),
},
},
"ordinals": {
SchemaProps: spec.SchemaProps{
Description: "Ordinals controls how the stateful set creates pod and persistent volume claim names. The default behavior assigns a number starting with zero and incremented by one for each additional replica requested. This requires the StatefulSetSlice feature gate to be enabled, which is alpha.",
Ref: ref("k8s.io/api/apps/v1beta2.StatefulSetOrdinals"),
},
},
},
Required: []string{"selector", "template", "serviceName"},
},
},
Dependencies: []string{
"k8s.io/api/apps/v1beta2.StatefulSetPersistentVolumeClaimRetentionPolicy", "k8s.io/api/apps/v1beta2.StatefulSetUpdateStrategy", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodTemplateSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
"k8s.io/api/apps/v1beta2.StatefulSetOrdinals", "k8s.io/api/apps/v1beta2.StatefulSetPersistentVolumeClaimRetentionPolicy", "k8s.io/api/apps/v1beta2.StatefulSetUpdateStrategy", "k8s.io/api/core/v1.PersistentVolumeClaim", "k8s.io/api/core/v1.PodTemplateSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
}
}

View File

@@ -128,6 +128,10 @@ func dropStatefulSetDisabledFields(newSS *apps.StatefulSet, oldSS *apps.Stateful
newSS.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable = nil
}
}
if !utilfeature.DefaultFeatureGate.Enabled(features.StatefulSetSlice) {
// Reset Spec.Ordinals to the default value (nil).
newSS.Spec.Ordinals = nil
}
}
// Validate validates a new StatefulSet.