Merge pull request #130233 from soltysh/statefulset_api

StatefulSet: add explicit validation for .spec.serviceName and mark the field optional
This commit is contained in:
Kubernetes Prow Robot 2025-03-13 13:27:46 -07:00 committed by GitHub
commit 94cc4babc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 82 additions and 11 deletions

View File

@ -2826,8 +2826,7 @@
},
"required": [
"selector",
"template",
"serviceName"
"template"
],
"type": "object"
},

View File

@ -1171,8 +1171,7 @@
},
"required": [
"selector",
"template",
"serviceName"
"template"
],
"type": "object"
},

View File

@ -198,6 +198,7 @@ type StatefulSetSpec struct {
// the network identity of the set. Pods get DNS/hostnames that follow the
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
// where "pod-specific-string" is managed by the StatefulSet controller.
// +optional
ServiceName string
// PodManagementPolicy controls how pods are created during initial scale up,

View File

@ -34,6 +34,12 @@ import (
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
)
// StatefulSetValidationOptions is a struct that can be passed to ValidateStatefulSetSpec to record the validate options
type StatefulSetValidationOptions struct {
// Allow invalid DNS1123 ServiceName
AllowInvalidServiceName bool
}
// ValidateStatefulSetName can be used to check whether the given StatefulSet name is valid.
// Prefix indicates this name will be used as part of generation, in which case
// trailing dashes are allowed.
@ -89,7 +95,7 @@ func ValidatePersistentVolumeClaimRetentionPolicy(policy *apps.StatefulSetPersis
}
// ValidateStatefulSetSpec tests if required fields in the StatefulSet spec are set.
func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions, setOpts StatefulSetValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
switch spec.PodManagementPolicy {
@ -134,6 +140,10 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(replicaStartOrdinal), fldPath.Child("ordinals.start"))...)
}
if !setOpts.AllowInvalidServiceName && len(spec.ServiceName) > 0 {
allErrs = append(allErrs, apivalidation.ValidateDNS1123Label(spec.ServiceName, fldPath.Child("serviceName"))...)
}
if spec.Selector == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
} else {
@ -164,7 +174,10 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
// ValidateStatefulSet validates a StatefulSet.
func ValidateStatefulSet(statefulSet *apps.StatefulSet, opts apivalidation.PodValidationOptions) field.ErrorList {
allErrs := apivalidation.ValidateObjectMeta(&statefulSet.ObjectMeta, true, ValidateStatefulSetName, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts)...)
setOpts := StatefulSetValidationOptions{
AllowInvalidServiceName: false, // require valid serviceNames in new StatefulSets
}
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts, setOpts)...)
return allErrs
}
@ -177,7 +190,10 @@ func ValidateStatefulSetUpdate(statefulSet, oldStatefulSet *apps.StatefulSet, op
// thing to do it delete such an instance, but if there is a finalizer, it
// would need to pass update validation. Name can't change anyway.
allErrs := apivalidation.ValidateObjectMetaUpdate(&statefulSet.ObjectMeta, &oldStatefulSet.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts)...)
setOpts := StatefulSetValidationOptions{
AllowInvalidServiceName: true, // serviceName is immutable, tolerate existing invalid names on update
}
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts, setOpts)...)
// statefulset updates aren't super common and general updates are likely to be touching spec, so we'll do this
// deep copy right away. This avoids mutating our inputs

View File

@ -195,6 +195,12 @@ func tweakPVCScalePolicy(t apps.PersistentVolumeClaimRetentionPolicyType) pvcPol
}
}
func tweakServiceName(name string) statefulSetTweak {
return func(ss *apps.StatefulSet) {
ss.Spec.ServiceName = name
}
}
func TestValidateStatefulSet(t *testing.T) {
validLabels := map[string]string{"a": "b"}
validPodTemplate := api.PodTemplate{
@ -520,6 +526,14 @@ func TestValidateStatefulSet(t *testing.T) {
errs: field.ErrorList{
field.Invalid(field.NewPath("spec", "ordinals.start"), nil, ""),
},
}, {
name: "invalid service name",
set: mkStatefulSet(&validPodTemplate,
tweakServiceName("Invalid.Name"),
),
errs: field.ErrorList{
field.Invalid(field.NewPath("spec", "serviceName"), "Invalid.Name", ""),
},
},
}
@ -595,7 +609,7 @@ func TestValidateStatefulSetMinReadySeconds(t *testing.T) {
for tcName, tc := range testCases {
t.Run(tcName, func(t *testing.T) {
errs := ValidateStatefulSetSpec(tc.ss, field.NewPath("spec", "minReadySeconds"),
corevalidation.PodValidationOptions{})
corevalidation.PodValidationOptions{}, StatefulSetValidationOptions{})
if tc.expectErr && len(errs) == 0 {
t.Errorf("Unexpected success")
}
@ -864,6 +878,10 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
name: "update existing instance with .spec.ordinals.start",
old: mkStatefulSet(&validPodTemplate),
update: mkStatefulSet(&validPodTemplate, tweakOrdinalsStart(3)),
}, {
name: "update with invalid .spec.serviceName",
old: mkStatefulSet(&validPodTemplate, tweakServiceName("Invalid.Name")),
update: mkStatefulSet(&validPodTemplate, tweakServiceName("Invalid.Name"), tweakReplicas(3)),
},
}

View File

@ -8251,7 +8251,7 @@ func schema_k8sio_api_apps_v1_StatefulSetSpec(ref common.ReferenceCallback) comm
},
},
},
Required: []string{"selector", "template", "serviceName"},
Required: []string{"selector", "template"},
},
},
Dependencies: []string{
@ -9403,7 +9403,7 @@ func schema_k8sio_api_apps_v1beta1_StatefulSetSpec(ref common.ReferenceCallback)
},
},
},
Required: []string{"template", "serviceName"},
Required: []string{"template"},
},
},
Dependencies: []string{
@ -11116,7 +11116,7 @@ func schema_k8sio_api_apps_v1beta2_StatefulSetSpec(ref common.ReferenceCallback)
},
},
},
Required: []string{"selector", "template", "serviceName"},
Required: []string{"selector", "template"},
},
},
Dependencies: []string{

View File

@ -716,6 +716,7 @@ message StatefulSetSpec {
// the network identity of the set. Pods get DNS/hostnames that follow the
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
// where "pod-specific-string" is managed by the StatefulSet controller.
// +optional
optional string serviceName = 5;
// podManagementPolicy controls how pods are created during initial scale up,

View File

@ -220,6 +220,7 @@ type StatefulSetSpec struct {
// the network identity of the set. Pods get DNS/hostnames that follow the
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
// where "pod-specific-string" is managed by the StatefulSet controller.
// +optional
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
// podManagementPolicy controls how pods are created during initial scale up,

View File

@ -462,6 +462,7 @@ message StatefulSetSpec {
// the network identity of the set. Pods get DNS/hostnames that follow the
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
// where "pod-specific-string" is managed by the StatefulSet controller.
// +optional
optional string serviceName = 5;
// podManagementPolicy controls how pods are created during initial scale up,

View File

@ -259,6 +259,7 @@ type StatefulSetSpec struct {
// the network identity of the set. Pods get DNS/hostnames that follow the
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
// where "pod-specific-string" is managed by the StatefulSet controller.
// +optional
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
// podManagementPolicy controls how pods are created during initial scale up,

View File

@ -761,6 +761,7 @@ message StatefulSetSpec {
// the network identity of the set. Pods get DNS/hostnames that follow the
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
// where "pod-specific-string" is managed by the StatefulSet controller.
// +optional
optional string serviceName = 5;
// podManagementPolicy controls how pods are created during initial scale up,

View File

@ -269,6 +269,7 @@ type StatefulSetSpec struct {
// the network identity of the set. Pods get DNS/hostnames that follow the
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
// where "pod-specific-string" is managed by the StatefulSet controller.
// +optional
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
// podManagementPolicy controls how pods are created during initial scale up,

View File

@ -768,3 +768,35 @@ func TestStatefulSetStartOrdinal(t *testing.T) {
})
}
}
func TestStatefulSetPodSubdomain(t *testing.T) {
tCtx, closeFn, rm, informers, c := scSetup(t)
defer closeFn()
ns := framework.CreateNamespaceOrDie(c, "test-pod-subdomain", t)
defer framework.DeleteNamespaceOrDie(c, ns, t)
cancel := runControllerAndInformers(tCtx, rm, informers)
defer cancel()
// create a headless service
serviceName := "test-service"
service := newHeadlessService(ns.Name)
service.Name = serviceName
createHeadlessService(t, c, service)
// create StatefulSet with the service name
sts := newSTS("sts", ns.Name, 3)
sts.Spec.ServiceName = serviceName
stss, _ := createSTSsPods(t, c, []*appsv1.StatefulSet{sts}, []*v1.Pod{})
sts = stss[0]
waitSTSStable(t, c, sts)
// get pods and verify subdomain
labelMap := labelMap()
podClient := c.CoreV1().Pods(ns.Name)
pods := getPods(t, podClient, labelMap)
for _, pod := range pods.Items {
if pod.Spec.Subdomain != serviceName {
t.Errorf("Pod %s has incorrect subdomain: got %s, want %s", pod.Name, pod.Spec.Subdomain, serviceName)
}
}
}