mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-02 08:17:26 +00:00
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:
commit
94cc4babc2
3
api/openapi-spec/swagger.json
generated
3
api/openapi-spec/swagger.json
generated
@ -2826,8 +2826,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"selector",
|
"selector",
|
||||||
"template",
|
"template"
|
||||||
"serviceName"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
3
api/openapi-spec/v3/apis__apps__v1_openapi.json
generated
3
api/openapi-spec/v3/apis__apps__v1_openapi.json
generated
@ -1171,8 +1171,7 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"selector",
|
"selector",
|
||||||
"template",
|
"template"
|
||||||
"serviceName"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
@ -198,6 +198,7 @@ type StatefulSetSpec struct {
|
|||||||
// the network identity of the set. Pods get DNS/hostnames that follow the
|
// the network identity of the set. Pods get DNS/hostnames that follow the
|
||||||
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
||||||
// where "pod-specific-string" is managed by the StatefulSet controller.
|
// where "pod-specific-string" is managed by the StatefulSet controller.
|
||||||
|
// +optional
|
||||||
ServiceName string
|
ServiceName string
|
||||||
|
|
||||||
// PodManagementPolicy controls how pods are created during initial scale up,
|
// PodManagementPolicy controls how pods are created during initial scale up,
|
||||||
|
@ -34,6 +34,12 @@ import (
|
|||||||
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
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.
|
// 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
|
// Prefix indicates this name will be used as part of generation, in which case
|
||||||
// trailing dashes are allowed.
|
// trailing dashes are allowed.
|
||||||
@ -89,7 +95,7 @@ func ValidatePersistentVolumeClaimRetentionPolicy(policy *apps.StatefulSetPersis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateStatefulSetSpec tests if required fields in the StatefulSet spec are set.
|
// 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{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
switch spec.PodManagementPolicy {
|
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"))...)
|
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 {
|
if spec.Selector == nil {
|
||||||
allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
|
allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
|
||||||
} else {
|
} else {
|
||||||
@ -164,7 +174,10 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
|
|||||||
// ValidateStatefulSet validates a StatefulSet.
|
// ValidateStatefulSet validates a StatefulSet.
|
||||||
func ValidateStatefulSet(statefulSet *apps.StatefulSet, opts apivalidation.PodValidationOptions) field.ErrorList {
|
func ValidateStatefulSet(statefulSet *apps.StatefulSet, opts apivalidation.PodValidationOptions) field.ErrorList {
|
||||||
allErrs := apivalidation.ValidateObjectMeta(&statefulSet.ObjectMeta, true, ValidateStatefulSetName, field.NewPath("metadata"))
|
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
|
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
|
// 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.
|
// would need to pass update validation. Name can't change anyway.
|
||||||
allErrs := apivalidation.ValidateObjectMetaUpdate(&statefulSet.ObjectMeta, &oldStatefulSet.ObjectMeta, field.NewPath("metadata"))
|
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
|
// 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
|
// deep copy right away. This avoids mutating our inputs
|
||||||
|
@ -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) {
|
func TestValidateStatefulSet(t *testing.T) {
|
||||||
validLabels := map[string]string{"a": "b"}
|
validLabels := map[string]string{"a": "b"}
|
||||||
validPodTemplate := api.PodTemplate{
|
validPodTemplate := api.PodTemplate{
|
||||||
@ -520,6 +526,14 @@ func TestValidateStatefulSet(t *testing.T) {
|
|||||||
errs: field.ErrorList{
|
errs: field.ErrorList{
|
||||||
field.Invalid(field.NewPath("spec", "ordinals.start"), nil, ""),
|
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 {
|
for tcName, tc := range testCases {
|
||||||
t.Run(tcName, func(t *testing.T) {
|
t.Run(tcName, func(t *testing.T) {
|
||||||
errs := ValidateStatefulSetSpec(tc.ss, field.NewPath("spec", "minReadySeconds"),
|
errs := ValidateStatefulSetSpec(tc.ss, field.NewPath("spec", "minReadySeconds"),
|
||||||
corevalidation.PodValidationOptions{})
|
corevalidation.PodValidationOptions{}, StatefulSetValidationOptions{})
|
||||||
if tc.expectErr && len(errs) == 0 {
|
if tc.expectErr && len(errs) == 0 {
|
||||||
t.Errorf("Unexpected success")
|
t.Errorf("Unexpected success")
|
||||||
}
|
}
|
||||||
@ -864,6 +878,10 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
|
|||||||
name: "update existing instance with .spec.ordinals.start",
|
name: "update existing instance with .spec.ordinals.start",
|
||||||
old: mkStatefulSet(&validPodTemplate),
|
old: mkStatefulSet(&validPodTemplate),
|
||||||
update: mkStatefulSet(&validPodTemplate, tweakOrdinalsStart(3)),
|
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)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
pkg/generated/openapi/zz_generated.openapi.go
generated
6
pkg/generated/openapi/zz_generated.openapi.go
generated
@ -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{
|
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{
|
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{
|
Dependencies: []string{
|
||||||
|
@ -716,6 +716,7 @@ message StatefulSetSpec {
|
|||||||
// the network identity of the set. Pods get DNS/hostnames that follow the
|
// the network identity of the set. Pods get DNS/hostnames that follow the
|
||||||
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
||||||
// where "pod-specific-string" is managed by the StatefulSet controller.
|
// where "pod-specific-string" is managed by the StatefulSet controller.
|
||||||
|
// +optional
|
||||||
optional string serviceName = 5;
|
optional string serviceName = 5;
|
||||||
|
|
||||||
// podManagementPolicy controls how pods are created during initial scale up,
|
// podManagementPolicy controls how pods are created during initial scale up,
|
||||||
|
@ -220,6 +220,7 @@ type StatefulSetSpec struct {
|
|||||||
// the network identity of the set. Pods get DNS/hostnames that follow the
|
// the network identity of the set. Pods get DNS/hostnames that follow the
|
||||||
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
||||||
// where "pod-specific-string" is managed by the StatefulSet controller.
|
// where "pod-specific-string" is managed by the StatefulSet controller.
|
||||||
|
// +optional
|
||||||
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
|
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
|
||||||
|
|
||||||
// podManagementPolicy controls how pods are created during initial scale up,
|
// podManagementPolicy controls how pods are created during initial scale up,
|
||||||
|
@ -462,6 +462,7 @@ message StatefulSetSpec {
|
|||||||
// the network identity of the set. Pods get DNS/hostnames that follow the
|
// the network identity of the set. Pods get DNS/hostnames that follow the
|
||||||
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
||||||
// where "pod-specific-string" is managed by the StatefulSet controller.
|
// where "pod-specific-string" is managed by the StatefulSet controller.
|
||||||
|
// +optional
|
||||||
optional string serviceName = 5;
|
optional string serviceName = 5;
|
||||||
|
|
||||||
// podManagementPolicy controls how pods are created during initial scale up,
|
// podManagementPolicy controls how pods are created during initial scale up,
|
||||||
|
@ -259,6 +259,7 @@ type StatefulSetSpec struct {
|
|||||||
// the network identity of the set. Pods get DNS/hostnames that follow the
|
// the network identity of the set. Pods get DNS/hostnames that follow the
|
||||||
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
||||||
// where "pod-specific-string" is managed by the StatefulSet controller.
|
// where "pod-specific-string" is managed by the StatefulSet controller.
|
||||||
|
// +optional
|
||||||
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
|
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
|
||||||
|
|
||||||
// podManagementPolicy controls how pods are created during initial scale up,
|
// podManagementPolicy controls how pods are created during initial scale up,
|
||||||
|
@ -761,6 +761,7 @@ message StatefulSetSpec {
|
|||||||
// the network identity of the set. Pods get DNS/hostnames that follow the
|
// the network identity of the set. Pods get DNS/hostnames that follow the
|
||||||
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
||||||
// where "pod-specific-string" is managed by the StatefulSet controller.
|
// where "pod-specific-string" is managed by the StatefulSet controller.
|
||||||
|
// +optional
|
||||||
optional string serviceName = 5;
|
optional string serviceName = 5;
|
||||||
|
|
||||||
// podManagementPolicy controls how pods are created during initial scale up,
|
// podManagementPolicy controls how pods are created during initial scale up,
|
||||||
|
@ -269,6 +269,7 @@ type StatefulSetSpec struct {
|
|||||||
// the network identity of the set. Pods get DNS/hostnames that follow the
|
// the network identity of the set. Pods get DNS/hostnames that follow the
|
||||||
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
// pattern: pod-specific-string.serviceName.default.svc.cluster.local
|
||||||
// where "pod-specific-string" is managed by the StatefulSet controller.
|
// where "pod-specific-string" is managed by the StatefulSet controller.
|
||||||
|
// +optional
|
||||||
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
|
ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"`
|
||||||
|
|
||||||
// podManagementPolicy controls how pods are created during initial scale up,
|
// podManagementPolicy controls how pods are created during initial scale up,
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user