Skip to content

Commit 8eb74b9

Browse files
committed
Add validation to StatefulSet's .spec.serviceName
.spec.serviceName field is injected into pod.spec.subDomain which requires values to be valid DNS1123 label, but statefulset validation never validates the field, if specifired. This can cause the controller to fail creating pods. Signed-off-by: Maciej Szulik <[email protected]>
1 parent 42abc2a commit 8eb74b9

File tree

3 files changed

+70
-4
lines changed

3 files changed

+70
-4
lines changed

pkg/apis/apps/validation/validation.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import (
3434
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
3535
)
3636

37+
// StatefulSetValidationOptions is a struct that can be passed to ValidateStatefulSetSpec to record the validate options
38+
type StatefulSetValidationOptions struct {
39+
// Allow invalid DNS1123 ServiceName
40+
AllowInvalidServiceName bool
41+
}
42+
3743
// ValidateStatefulSetName can be used to check whether the given StatefulSet name is valid.
3844
// Prefix indicates this name will be used as part of generation, in which case
3945
// trailing dashes are allowed.
@@ -89,7 +95,7 @@ func ValidatePersistentVolumeClaimRetentionPolicy(policy *apps.StatefulSetPersis
8995
}
9096

9197
// ValidateStatefulSetSpec tests if required fields in the StatefulSet spec are set.
92-
func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
98+
func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions, setOpts StatefulSetValidationOptions) field.ErrorList {
9399
allErrs := field.ErrorList{}
94100

95101
switch spec.PodManagementPolicy {
@@ -134,6 +140,10 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
134140
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(replicaStartOrdinal), fldPath.Child("ordinals.start"))...)
135141
}
136142

143+
if !setOpts.AllowInvalidServiceName && len(spec.ServiceName) > 0 {
144+
allErrs = append(allErrs, apivalidation.ValidateDNS1123Label(spec.ServiceName, fldPath.Child("serviceName"))...)
145+
}
146+
137147
if spec.Selector == nil {
138148
allErrs = append(allErrs, field.Required(fldPath.Child("selector"), ""))
139149
} else {
@@ -164,7 +174,10 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
164174
// ValidateStatefulSet validates a StatefulSet.
165175
func ValidateStatefulSet(statefulSet *apps.StatefulSet, opts apivalidation.PodValidationOptions) field.ErrorList {
166176
allErrs := apivalidation.ValidateObjectMeta(&statefulSet.ObjectMeta, true, ValidateStatefulSetName, field.NewPath("metadata"))
167-
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts)...)
177+
setOpts := StatefulSetValidationOptions{
178+
AllowInvalidServiceName: false, // require valid serviceNames in new StatefulSets
179+
}
180+
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts, setOpts)...)
168181
return allErrs
169182
}
170183

@@ -177,7 +190,10 @@ func ValidateStatefulSetUpdate(statefulSet, oldStatefulSet *apps.StatefulSet, op
177190
// thing to do it delete such an instance, but if there is a finalizer, it
178191
// would need to pass update validation. Name can't change anyway.
179192
allErrs := apivalidation.ValidateObjectMetaUpdate(&statefulSet.ObjectMeta, &oldStatefulSet.ObjectMeta, field.NewPath("metadata"))
180-
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts)...)
193+
setOpts := StatefulSetValidationOptions{
194+
AllowInvalidServiceName: true, // serviceName is immutable, tolerate existing invalid names on update
195+
}
196+
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts, setOpts)...)
181197

182198
// statefulset updates aren't super common and general updates are likely to be touching spec, so we'll do this
183199
// deep copy right away. This avoids mutating our inputs

pkg/apis/apps/validation/validation_test.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ func tweakPVCScalePolicy(t apps.PersistentVolumeClaimRetentionPolicyType) pvcPol
195195
}
196196
}
197197

198+
func tweakServiceName(name string) statefulSetTweak {
199+
return func(ss *apps.StatefulSet) {
200+
ss.Spec.ServiceName = name
201+
}
202+
}
203+
198204
func TestValidateStatefulSet(t *testing.T) {
199205
validLabels := map[string]string{"a": "b"}
200206
validPodTemplate := api.PodTemplate{
@@ -520,6 +526,14 @@ func TestValidateStatefulSet(t *testing.T) {
520526
errs: field.ErrorList{
521527
field.Invalid(field.NewPath("spec", "ordinals.start"), nil, ""),
522528
},
529+
}, {
530+
name: "invalid service name",
531+
set: mkStatefulSet(&validPodTemplate,
532+
tweakServiceName("Invalid.Name"),
533+
),
534+
errs: field.ErrorList{
535+
field.Invalid(field.NewPath("spec", "serviceName"), "Invalid.Name", ""),
536+
},
523537
},
524538
}
525539

@@ -595,7 +609,7 @@ func TestValidateStatefulSetMinReadySeconds(t *testing.T) {
595609
for tcName, tc := range testCases {
596610
t.Run(tcName, func(t *testing.T) {
597611
errs := ValidateStatefulSetSpec(tc.ss, field.NewPath("spec", "minReadySeconds"),
598-
corevalidation.PodValidationOptions{})
612+
corevalidation.PodValidationOptions{}, StatefulSetValidationOptions{})
599613
if tc.expectErr && len(errs) == 0 {
600614
t.Errorf("Unexpected success")
601615
}
@@ -864,6 +878,10 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
864878
name: "update existing instance with .spec.ordinals.start",
865879
old: mkStatefulSet(&validPodTemplate),
866880
update: mkStatefulSet(&validPodTemplate, tweakOrdinalsStart(3)),
881+
}, {
882+
name: "update with invalid .spec.serviceName",
883+
old: mkStatefulSet(&validPodTemplate, tweakServiceName("Invalid.Name")),
884+
update: mkStatefulSet(&validPodTemplate, tweakServiceName("Invalid.Name"), tweakReplicas(3)),
867885
},
868886
}
869887

test/integration/statefulset/statefulset_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,3 +768,35 @@ func TestStatefulSetStartOrdinal(t *testing.T) {
768768
})
769769
}
770770
}
771+
func TestStatefulSetPodSubdomain(t *testing.T) {
772+
tCtx, closeFn, rm, informers, c := scSetup(t)
773+
defer closeFn()
774+
ns := framework.CreateNamespaceOrDie(c, "test-pod-subdomain", t)
775+
defer framework.DeleteNamespaceOrDie(c, ns, t)
776+
cancel := runControllerAndInformers(tCtx, rm, informers)
777+
defer cancel()
778+
779+
// create a headless service
780+
serviceName := "test-service"
781+
service := newHeadlessService(ns.Name)
782+
service.Name = serviceName
783+
createHeadlessService(t, c, service)
784+
785+
// create StatefulSet with the service name
786+
sts := newSTS("sts", ns.Name, 3)
787+
sts.Spec.ServiceName = serviceName
788+
stss, _ := createSTSsPods(t, c, []*appsv1.StatefulSet{sts}, []*v1.Pod{})
789+
sts = stss[0]
790+
waitSTSStable(t, c, sts)
791+
792+
// get pods and verify subdomain
793+
labelMap := labelMap()
794+
podClient := c.CoreV1().Pods(ns.Name)
795+
pods := getPods(t, podClient, labelMap)
796+
797+
for _, pod := range pods.Items {
798+
if pod.Spec.Subdomain != serviceName {
799+
t.Errorf("Pod %s has incorrect subdomain: got %s, want %s", pod.Name, pod.Spec.Subdomain, serviceName)
800+
}
801+
}
802+
}

0 commit comments

Comments
 (0)