Merge pull request #132339 from adrianmoisey/relaxed-validation-for-services-names

KEP-5311 Relaxed validation for Services names
This commit is contained in:
Kubernetes Prow Robot 2025-07-08 09:03:26 -07:00 committed by GitHub
commit 9fbd2dae14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 534 additions and 21 deletions

View file

@ -5924,9 +5924,12 @@ var supportedServiceIPFamilyPolicy = sets.New(
core.IPFamilyPolicyRequireDualStack)
// ValidateService tests if required fields/annotations of a Service are valid.
func ValidateService(service, oldService *core.Service) field.ErrorList {
func validateService(service, oldService *core.Service) field.ErrorList {
metaPath := field.NewPath("metadata")
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName, metaPath)
// Don't validate ObjectMeta here - that is handled in the ValidateServiceCreate/ValidateServiceUpdate
// functions which call ValidateObjectMeta and ValidateObjectMetaUpdate respectively.
var allErrs field.ErrorList
topologyHintsVal, topologyHintsSet := service.Annotations[core.DeprecatedAnnotationTopologyAwareHints]
topologyModeVal, topologyModeSet := service.Annotations[core.AnnotationTopologyMode]
@ -6276,7 +6279,17 @@ func validateServiceTrafficDistribution(service *core.Service) field.ErrorList {
// ValidateServiceCreate validates Services as they are created.
func ValidateServiceCreate(service *core.Service) field.ErrorList {
return ValidateService(service, nil)
metaPath := field.NewPath("metadata")
// KEP-5311 Relaxed validation for Services names
validateServiceNameFunc := ValidateServiceName
if utilfeature.DefaultFeatureGate.Enabled(features.RelaxedServiceNameValidation) {
validateServiceNameFunc = apimachineryvalidation.NameIsDNSLabel
}
allErrs := ValidateObjectMeta(&service.ObjectMeta, true, validateServiceNameFunc, metaPath)
return append(allErrs, validateService(service, nil)...)
}
// ValidateServiceUpdate tests if required fields in the service are set during an update
@ -6299,7 +6312,7 @@ func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
allErrs = append(allErrs, validateServiceExternalTrafficFieldsUpdate(oldService, service)...)
return append(allErrs, ValidateService(service, oldService)...)
return append(allErrs, validateService(service, oldService)...)
}
// ValidateServiceStatusUpdate tests if required fields in the Service are set when updating status.

View file

@ -15474,11 +15474,12 @@ func TestValidateServiceCreate(t *testing.T) {
preferDualStack := core.IPFamilyPolicyPreferDualStack
testCases := []struct {
name string
tweakSvc func(svc *core.Service) // given a basic valid service, each test case can customize it
numErrs int
legacyIPs bool
newTrafficDist bool
name string
tweakSvc func(svc *core.Service) // given a basic valid service, each test case can customize it
numErrs int
legacyIPs bool
newTrafficDist bool
relaxedServiceNames bool
}{{
name: "default",
tweakSvc: func(s *core.Service) {},
@ -16764,12 +16765,28 @@ func TestValidateServiceCreate(t *testing.T) {
s.Spec.TrafficDistribution = ptr.To("PreferSameNode")
},
numErrs: 1,
}, {
name: "valid: service name begins with a digit feature gate enabled",
relaxedServiceNames: true,
tweakSvc: func(s *core.Service) {
s.Name = "1-test-service"
},
numErrs: 0,
}, {
name: "invalid: service name begins with a digit feature gate disabled",
relaxedServiceNames: false,
tweakSvc: func(s *core.Service) {
s.Name = "1-test-service"
},
numErrs: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PreferSameTrafficDistribution, tc.newTrafficDist)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RelaxedServiceNameValidation, tc.relaxedServiceNames)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, !tc.legacyIPs)
svc := makeValidService()
tc.tweakSvc(&svc)
@ -18214,9 +18231,10 @@ func TestValidateServiceUpdate(t *testing.T) {
preferDualStack := core.IPFamilyPolicyPreferDualStack
singleStack := core.IPFamilyPolicySingleStack
testCases := []struct {
name string
tweakSvc func(oldSvc, newSvc *core.Service) // given basic valid services, each test case can customize them
numErrs int
name string
tweakSvc func(oldSvc, newSvc *core.Service) // given basic valid services, each test case can customize them
numErrs int
relaxedServiceNames bool
}{{
name: "no change",
tweakSvc: func(oldSvc, newSvc *core.Service) {
@ -19477,12 +19495,22 @@ func TestValidateServiceUpdate(t *testing.T) {
newSvc.Annotations[core.AnnotationLoadBalancerSourceRangesKey] = "010.0.0.0/8, 1.2.3.0/24"
},
numErrs: 1,
}, {
name: "can modify a pre-existing relaxed service name without error",
tweakSvc: func(oldSvc, newSvc *core.Service) {
oldSvc.Name = "1-test-service"
newSvc.Name = "1-test-service"
newSvc.Labels["foo"] = "bar"
},
relaxedServiceNames: false,
numErrs: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StrictIPCIDRValidation, true)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RelaxedServiceNameValidation, tc.relaxedServiceNames)
oldSvc := makeValidService()
newSvc := makeValidService()

View file

@ -27,9 +27,11 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
api "k8s.io/kubernetes/pkg/apis/core"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/networking"
"k8s.io/kubernetes/pkg/features"
netutils "k8s.io/utils/net"
"k8s.io/utils/ptr"
)
@ -283,6 +285,9 @@ type IngressValidationOptions struct {
// AllowInvalidWildcardHostRule indicates whether invalid rule values are allowed in rules with wildcard hostnames
AllowInvalidWildcardHostRule bool
// AllowRelaxedServiceNameValidation indicates if the backend service name can be validated with apimachineryvalidation.NameIsDNSLabel
AllowRelaxedServiceNameValidation bool
}
// ValidateIngress validates Ingresses on create and update.
@ -296,8 +301,9 @@ func validateIngress(ingress *networking.Ingress, opts IngressValidationOptions)
func ValidateIngressCreate(ingress *networking.Ingress) field.ErrorList {
allErrs := field.ErrorList{}
opts := IngressValidationOptions{
AllowInvalidSecretName: false,
AllowInvalidWildcardHostRule: false,
AllowInvalidSecretName: false,
AllowInvalidWildcardHostRule: false,
AllowRelaxedServiceNameValidation: allowRelaxedServiceNameValidation(nil),
}
allErrs = append(allErrs, validateIngress(ingress, opts)...)
annotationVal, annotationIsSet := ingress.Annotations[annotationIngressClass]
@ -312,8 +318,9 @@ func ValidateIngressCreate(ingress *networking.Ingress) field.ErrorList {
func ValidateIngressUpdate(ingress, oldIngress *networking.Ingress) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&ingress.ObjectMeta, &oldIngress.ObjectMeta, field.NewPath("metadata"))
opts := IngressValidationOptions{
AllowInvalidSecretName: allowInvalidSecretName(oldIngress),
AllowInvalidWildcardHostRule: allowInvalidWildcardHostRule(oldIngress),
AllowInvalidSecretName: allowInvalidSecretName(oldIngress),
AllowInvalidWildcardHostRule: allowInvalidWildcardHostRule(oldIngress),
AllowRelaxedServiceNameValidation: allowRelaxedServiceNameValidation(oldIngress),
}
allErrs = append(allErrs, validateIngress(ingress, opts)...)
@ -513,7 +520,13 @@ func validateIngressBackend(backend *networking.IngressBackend, fldPath *field.P
if len(backend.Service.Name) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("service", "name"), ""))
} else {
for _, msg := range apivalidation.ValidateServiceName(backend.Service.Name, false) {
validationFunc := apivalidation.ValidateServiceName
if opts.AllowRelaxedServiceNameValidation {
validationFunc = apimachineryvalidation.NameIsDNSLabel
}
for _, msg := range validationFunc(backend.Service.Name, false) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("service", "name"), backend.Service.Name, msg))
}
}
@ -685,6 +698,37 @@ func allowInvalidWildcardHostRule(oldIngress *networking.Ingress) bool {
return false
}
func allowRelaxedServiceNameValidation(oldIngress *networking.Ingress) bool {
// Early exit if the feature gate is enabled, as it allows relaxed validation
if utilfeature.DefaultFeatureGate.Enabled(features.RelaxedServiceNameValidation) {
return true
}
// Early exit if no old Ingress is provided
if oldIngress == nil {
return false
}
// If feature gate is disabled, check if any service names in the old Ingresss
for _, rule := range oldIngress.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
if path.Backend.Service == nil {
continue
}
serviceName := path.Backend.Service.Name
// If a name doesn't validate with apimachineryvalidation.NameIsDNS1035Label, but does validate with apimachineryvalidation.NameIsDNSLabel,
// then we allow it to be used as a Service name in an Ingress.
dnsLabelValidationErrors := apimachineryvalidation.NameIsDNSLabel(serviceName, false)
dns1035LabelValidationErrors := apimachineryvalidation.NameIsDNS1035Label(serviceName, false)
if len(dnsLabelValidationErrors) == 0 && len(dns1035LabelValidationErrors) > 0 {
return true
}
}
}
return false
}
// ValidateIPAddressName validates that the name is the decimal representation of an IP address.
// IPAddress does not support generating names, prefix is not considered.
func ValidateIPAddressName(name string, prefix bool) []string {

View file

@ -1016,8 +1016,9 @@ func TestValidateIngressCreate(t *testing.T) {
}
testCases := map[string]struct {
tweakIngress func(ingress *networking.Ingress)
expectedErrs field.ErrorList
tweakIngress func(ingress *networking.Ingress)
expectedErrs field.ErrorList
relaxedServiceName bool
}{
"class field set": {
tweakIngress: func(ingress *networking.Ingress) {
@ -1150,10 +1151,76 @@ func TestValidateIngressCreate(t *testing.T) {
},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("http").Child("paths").Index(0).Child("path"), "foo", `must be an absolute path`)},
},
"create service name with RelaxedServiceNameValidation feature gate enabled": {
tweakIngress: func(ingress *networking.Ingress) {
ingress.Spec.Rules = []networking.IngressRule{{
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo",
PathType: &exactPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
},
relaxedServiceName: true,
},
"create default service name with RelaxedServiceNameValidation feature gate enabled": {
tweakIngress: func(ingress *networking.Ingress) {
ingress.Spec.DefaultBackend = &networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1-test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
}
},
relaxedServiceName: true,
},
"create service name with RelaxedServiceNameValidation feature gate disabled": {
tweakIngress: func(ingress *networking.Ingress) {
ingress.Spec.Rules = []networking.IngressRule{{
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo",
PathType: &exactPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1-test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("http").Child("paths").Index(0).Child("backend").Child("service").Child("name"), "1-test-service", `a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')`)},
},
"create default service name with RelaxedServiceNameValidation feature gate disabled": {
tweakIngress: func(ingress *networking.Ingress) {
ingress.Spec.DefaultBackend = &networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1-test-default-backend",
Port: networking.ServiceBackendPort{Number: 80},
},
}
},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec").Child("defaultBackend").Child("service").Child("name"), "1-test-default-backend", `a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')`)},
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RelaxedServiceNameValidation, testCase.relaxedServiceName)
newIngress := baseIngress.DeepCopy()
testCase.tweakIngress(newIngress)
errs := ValidateIngressCreate(newIngress)
@ -1199,8 +1266,9 @@ func TestValidateIngressUpdate(t *testing.T) {
}
testCases := map[string]struct {
tweakIngresses func(newIngress, oldIngress *networking.Ingress)
expectedErrs field.ErrorList
tweakIngresses func(newIngress, oldIngress *networking.Ingress)
expectedErrs field.ErrorList
relaxedServiceName bool
}{
"class field set": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
@ -1556,6 +1624,160 @@ func TestValidateIngressUpdate(t *testing.T) {
}}
},
},
"update service to conform to relaxed service name - RelaxedServiceNameValidation disabled": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1-test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
},
expectedErrs: field.ErrorList{field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("http").Child("paths").Index(0).Child("backend").Child("service").Child("name"), "1-test-service", `a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')`)},
},
"update service to conform to relaxed service name - RelaxedServiceNameValidation enabled": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/foo",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1-test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
},
relaxedServiceName: true,
},
"updating an already existing relaxed validation service name with RelaxedServiceNameValidation disabled": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1-test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "2-test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
},
},
"updating an already existing relaxed validation service name to a non-relaxed name with RelaxedServiceNameValidation disabled": {
tweakIngresses: func(newIngress, oldIngress *networking.Ingress) {
oldIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "1-test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
newIngress.Spec.Rules = []networking.IngressRule{{
Host: "foo.bar.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Path: "/",
PathType: &implementationPathType,
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: "test-service",
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}}
},
},
}
for name, testCase := range testCases {
@ -1563,6 +1785,7 @@ func TestValidateIngressUpdate(t *testing.T) {
newIngress := baseIngress.DeepCopy()
oldIngress := baseIngress.DeepCopy()
testCase.tweakIngresses(newIngress, oldIngress)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RelaxedServiceNameValidation, testCase.relaxedServiceName)
errs := ValidateIngressUpdate(newIngress, oldIngress)
@ -2647,3 +2870,70 @@ func TestValidateServiceCIDRUpdate(t *testing.T) {
})
}
}
func TestAllowRelaxedServiceNameValidation(t *testing.T) {
basicIngress := func(serviceNames ...string) *networking.Ingress {
if len(serviceNames) == 0 {
return &networking.Ingress{Spec: networking.IngressSpec{Rules: nil}}
}
rules := make([]networking.IngressRule, len(serviceNames))
for i, name := range serviceNames {
rules[i] = networking.IngressRule{
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{{
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: name,
Port: networking.ServiceBackendPort{Number: 80},
},
},
}},
},
},
}
}
return &networking.Ingress{Spec: networking.IngressSpec{Rules: rules}}
}
tests := []struct {
name string
ingress *networking.Ingress
expect bool
}{
{
name: "nil ingress",
ingress: nil,
expect: false,
},
{
name: "no rules",
ingress: basicIngress(),
expect: false,
},
{
name: "service name is valid DNS1035 and DNS1123",
ingress: basicIngress("validname"),
expect: false,
},
{
name: "service name is valid DNS1123 but not DNS1035 (contains dash, starts with digit)",
ingress: basicIngress("1abc-def"),
expect: true,
},
{
name: "multiple rules, one triggers relaxed validation",
ingress: basicIngress("validname", "1abc-def"),
expect: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := allowRelaxedServiceNameValidation(tc.ingress)
if got != tc.expect {
t.Errorf("allowRelaxedServiceNameValidation() = %v, want %v", got, tc.expect)
}
})
}
}

View file

@ -706,6 +706,12 @@ const (
// Allow almost all printable ASCII characters in environment variables
RelaxedEnvironmentVariableValidation featuregate.Feature = "RelaxedEnvironmentVariableValidation"
// owner: @adrianmoisey
// kep: https://kep.k8s.io/5311
//
// Relaxed DNS search string validation.
RelaxedServiceNameValidation featuregate.Feature = "RelaxedServiceNameValidation"
// owner: @zhangweikop
//
// Enable kubelet tls server to update certificate if the specified certificate files are changed.
@ -1487,6 +1493,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
{Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.37
},
RelaxedServiceNameValidation: {
{Version: version.MustParse("1.34"), Default: false, PreRelease: featuregate.Alpha},
},
ReloadKubeletServerCertificateFile: {
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
},

View file

@ -1195,6 +1195,12 @@
lockToDefault: true
preRelease: GA
version: "1.34"
- name: RelaxedServiceNameValidation
versionedSpecs:
- default: false
lockToDefault: false
preRelease: Alpha
version: "1.34"
- name: ReloadKubeletServerCertificateFile
versionedSpecs:
- default: true

View file

@ -25,6 +25,7 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/test/e2e/framework"
e2enode "k8s.io/kubernetes/test/e2e/framework/node"
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
@ -654,6 +655,35 @@ var _ = common.SIGDescribe("DNS", func() {
}
validateDNSResults(ctx, f, pod, append(agnhostFileNames, jessieFileNames...))
})
framework.It("should work with a service name that starts with a digit", framework.WithFeatureGate(features.RelaxedServiceNameValidation), func(ctx context.Context) {
svcName := "1kubernetes"
svc := v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
},
Spec: v1.ServiceSpec{
Ports: []v1.ServicePort{{Port: 443}},
},
}
createServiceReportErr(ctx, f.ClientSet, f.Namespace.Name, &svc)
namesToResolve := []string{
fmt.Sprintf("%s.%s.svc.%s", svcName, f.Namespace.Name, framework.TestContext.ClusterDNSDomain),
}
agnhostProbeCmd, agnhostFileNames := createProbeCommand(namesToResolve, nil, "", "agnhost", f.Namespace.Name, framework.TestContext.ClusterDNSDomain, framework.TestContext.ClusterIsIPv6())
agnhostProber := dnsQuerier{name: "agnhost", image: imageutils.Agnhost, cmd: agnhostProbeCmd}
jessieProbeCmd, jessieFileNames := createProbeCommand(namesToResolve, nil, "", "jessie", f.Namespace.Name, framework.TestContext.ClusterDNSDomain, framework.TestContext.ClusterIsIPv6())
jessieProber := dnsQuerier{name: "jessie", image: imageutils.JessieDnsutils, cmd: jessieProbeCmd}
ginkgo.By("Running these commands on agnhost: " + agnhostProbeCmd + "\n")
ginkgo.By("Running these commands on jessie: " + jessieProbeCmd + "\n")
// Run a pod which probes DNS and exposes the results by HTTP.
ginkgo.By("creating a pod to probe DNS")
pod := createDNSPod(f.Namespace.Name, []dnsQuerier{agnhostProber, jessieProber}, dnsTestPodHostName, dnsTestServiceName)
validateDNSResults(ctx, f, pod, append(agnhostFileNames, jessieFileNames...))
})
})
var _ = common.SIGDescribe("DNS HostNetwork", func() {

View file

@ -1215,3 +1215,95 @@ func Test_ServiceWatchUntil(t *testing.T) {
}
t.Logf("Service %s deleted", testSvcName)
}
func Test_ServiceValidation_FeatureGateEnableDisable(t *testing.T) {
////////////////////////////////////////////////////////////////////////////
// Start kube-apiserver with RelaxedServiceNameValidation feature-gate
// enabled.
////////////////////////////////////////////////////////////////////////////
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RelaxedServiceNameValidation, true)
sharedEtcd := framework.SharedEtcd()
server1 := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), sharedEtcd)
client1, err := clientset.NewForConfig(server1.ClientConfig)
if err != nil {
t.Fatalf("Error creating clientset: %v", err)
}
////////////////////////////////////////////////////////////////////////////
// Create services with names that start with a digit and a letter.
//
// Assert that the services are created successfully with the feature gate enabled
////////////////////////////////////////////////////////////////////////////
ns := framework.CreateNamespaceOrDie(client1, "test-service-traffic-distribution", t)
makeService := func(serviceName string) *corev1.Service {
return &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: ns.GetName(),
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{{Port: 443}},
},
}
}
// Expected to pass, as the feature gate is enabled
_, err = client1.CoreV1().Services(ns.Name).Create(t.Context(), makeService("test-service-1"), metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create test service: %v", err)
}
// Expected to pass, as the feature gate is enabled
_, err = client1.CoreV1().Services(ns.Name).Create(t.Context(), makeService("9-test-service-1"), metav1.CreateOptions{})
if err != nil {
t.Fatalf("Successfully created service, but shouldn't have: %v", err)
}
////////////////////////////////////////////////////////////////////////////
// Restart the kube-apiserver with RelaxedServiceNameValidation feature-gate
// disabled.
//
// Assert that the services are created using previous validation only
////////////////////////////////////////////////////////////////////////////
server1.TearDownFn()
featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.34"))
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RelaxedServiceNameValidation, false)
server2 := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), sharedEtcd)
client2, err := clientset.NewForConfig(server2.ClientConfig)
if err != nil {
t.Fatalf("Error creating clientset: %v", err)
}
// Expected to pass, as the feature gate is disabled
_, err = client2.CoreV1().Services(ns.Name).Create(t.Context(), makeService("test-service-2"), metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create test service: %v", err)
}
// Expected to fail, as the feature gate is disabled and this name requires relaxed validation
_, err = client2.CoreV1().Services(ns.Name).Create(t.Context(), makeService("9-test-service-2"), metav1.CreateOptions{})
if err == nil {
t.Fatalf("Successfully created service, but shouldn't have: %v", err)
}
////////////////////////////////////////////////////////////////////////////
// Assert that the services created prior to the feature gate being disabled
// can still be patched successfully even though it requires relaxed validation.
////////////////////////////////////////////////////////////////////////////
// Expected to pass as the service was created before the feature gate was disabled
patch := []byte(`{"spec":{"selector":{"foo":"baz"}}}`)
_, err = client2.CoreV1().Services(ns.Name).Patch(t.Context(), "9-test-service-1", types.StrategicMergePatchType, patch, metav1.PatchOptions{})
if err != nil {
t.Fatalf("Failed to patch selector of service '9-test-service-1': %v", err)
}
server2.TearDownFn()
}