Skip to content

Commit 8e3adc4

Browse files
authored
KEP-4427 : AllowRelaxedDNSSearchValidation (kubernetes#127167)
* KEP-4427 : AllowRelaxedDNSSearchValidation * Add e2e test with feature gate to test KEP-4427 RelaxedDNSSearchValidation * Add more validatePodDNSConfig test cases Also update Regex to match the case we want. Thanks Tim and Antonio!
1 parent 9e59765 commit 8e3adc4

File tree

11 files changed

+397
-4
lines changed

11 files changed

+397
-4
lines changed

pkg/api/pod/testing/make.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ func SetDNSPolicy(policy api.DNSPolicy) Tweak {
165165
}
166166
}
167167

168+
func SetDNSConfig(config *api.PodDNSConfig) Tweak {
169+
return func(pod *api.Pod) {
170+
pod.Spec.DNSConfig = config
171+
}
172+
}
173+
168174
func SetRestartPolicy(policy api.RestartPolicy) Tweak {
169175
return func(pod *api.Pod) {
170176
pod.Spec.RestartPolicy = policy

pkg/api/pod/util.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ func GetValidationOptionsFromPodSpecAndMeta(podSpec, oldPodSpec *api.PodSpec, po
391391
// If old spec uses relaxed validation or enabled the RelaxedEnvironmentVariableValidation feature gate,
392392
// we must allow it
393393
opts.AllowRelaxedEnvironmentVariableValidation = useRelaxedEnvironmentVariableValidation(podSpec, oldPodSpec)
394+
opts.AllowRelaxedDNSSearchValidation = useRelaxedDNSSearchValidation(oldPodSpec)
394395

395396
if oldPodSpec != nil {
396397
// if old spec has status.hostIPs downwardAPI set, we must allow it
@@ -448,6 +449,30 @@ func useRelaxedEnvironmentVariableValidation(podSpec, oldPodSpec *api.PodSpec) b
448449
return false
449450
}
450451

452+
func useRelaxedDNSSearchValidation(oldPodSpec *api.PodSpec) bool {
453+
// Return true early if feature gate is enabled
454+
if utilfeature.DefaultFeatureGate.Enabled(features.RelaxedDNSSearchValidation) {
455+
return true
456+
}
457+
458+
// Return false early if there is no DNSConfig or Searches.
459+
if oldPodSpec == nil || oldPodSpec.DNSConfig == nil || oldPodSpec.DNSConfig.Searches == nil {
460+
return false
461+
}
462+
463+
return hasDotOrUnderscore(oldPodSpec.DNSConfig.Searches)
464+
}
465+
466+
// Helper function to check if any domain is a dot or contains an underscore.
467+
func hasDotOrUnderscore(searches []string) bool {
468+
for _, domain := range searches {
469+
if domain == "." || strings.Contains(domain, "_") {
470+
return true
471+
}
472+
}
473+
return false
474+
}
475+
451476
func gatherPodEnvVarNames(podSpec *api.PodSpec) sets.Set[string] {
452477
podEnvVarNames := sets.Set[string]{}
453478

pkg/apis/core/validation/validation.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ func ValidateQualifiedName(value string, fldPath *field.Path) field.ErrorList {
145145
return allErrs
146146
}
147147

148+
// ValidateDNS1123SubdomainWithUnderScore validates that a name is a proper DNS subdomain but allows for an underscore in the string
149+
func ValidateDNS1123SubdomainWithUnderScore(value string, fldPath *field.Path) field.ErrorList {
150+
allErrs := field.ErrorList{}
151+
for _, msg := range validation.IsDNS1123SubdomainWithUnderscore(value) {
152+
allErrs = append(allErrs, field.Invalid(fldPath, value, msg))
153+
}
154+
return allErrs
155+
}
156+
148157
// ValidateDNS1123Subdomain validates that a name is a proper DNS subdomain.
149158
func ValidateDNS1123Subdomain(value string, fldPath *field.Path) field.ErrorList {
150159
allErrs := field.ErrorList{}
@@ -3779,10 +3788,18 @@ func validatePodDNSConfig(dnsConfig *core.PodDNSConfig, dnsPolicy *core.DNSPolic
37793788
if len(strings.Join(dnsConfig.Searches, " ")) > MaxDNSSearchListChars {
37803789
allErrs = append(allErrs, field.Invalid(fldPath.Child("searches"), dnsConfig.Searches, fmt.Sprintf("must not have more than %v characters (including spaces) in the search list", MaxDNSSearchListChars)))
37813790
}
3791+
37823792
for i, search := range dnsConfig.Searches {
3783-
// it is fine to have a trailing dot
3784-
search = strings.TrimSuffix(search, ".")
3785-
allErrs = append(allErrs, ValidateDNS1123Subdomain(search, fldPath.Child("searches").Index(i))...)
3793+
if opts.AllowRelaxedDNSSearchValidation {
3794+
if search != "." {
3795+
search = strings.TrimSuffix(search, ".")
3796+
allErrs = append(allErrs, ValidateDNS1123SubdomainWithUnderScore(search, fldPath.Child("searches").Index(i))...)
3797+
}
3798+
} else {
3799+
search = strings.TrimSuffix(search, ".")
3800+
allErrs = append(allErrs, ValidateDNS1123Subdomain(search, fldPath.Child("searches").Index(i))...)
3801+
}
3802+
37863803
}
37873804
// Validate options.
37883805
for i, option := range dnsConfig.Options {
@@ -4034,6 +4051,8 @@ type PodValidationOptions struct {
40344051
AllowRelaxedEnvironmentVariableValidation bool
40354052
// Allow the use of the ImageVolumeSource API.
40364053
AllowImageVolumeSource bool
4054+
// Allow the use of a relaxed DNS search
4055+
AllowRelaxedDNSSearchValidation bool
40374056
}
40384057

40394058
// validatePodMetadataAndSpec tests if required fields in the pod.metadata and pod.spec are set,

pkg/apis/core/validation/validation_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24193,6 +24193,172 @@ func TestValidateSleepAction(t *testing.T) {
2419324193
}
2419424194
}
2419524195

24196+
// TODO: merge these test to TestValidatePodSpec after AllowRelaxedDNSSearchValidation feature graduates to Beta
24197+
func TestValidatePodDNSConfigWithRelaxedSearchDomain(t *testing.T) {
24198+
testCases := []struct {
24199+
name string
24200+
expectError bool
24201+
featureEnabled bool
24202+
dnsConfig *core.PodDNSConfig
24203+
}{
24204+
{
24205+
name: "beginswith underscore, contains underscore, featuregate enabled",
24206+
expectError: false,
24207+
featureEnabled: true,
24208+
dnsConfig: &core.PodDNSConfig{Searches: []string{"_sip._tcp.abc_d.example.com"}},
24209+
},
24210+
{
24211+
name: "contains underscore, featuregate enabled",
24212+
expectError: false,
24213+
featureEnabled: true,
24214+
dnsConfig: &core.PodDNSConfig{Searches: []string{"abc_d.example.com"}},
24215+
},
24216+
{
24217+
name: "is dot, featuregate enabled",
24218+
expectError: false,
24219+
featureEnabled: true,
24220+
dnsConfig: &core.PodDNSConfig{Searches: []string{"."}},
24221+
},
24222+
{
24223+
name: "two dots, featuregate enabled",
24224+
expectError: true,
24225+
featureEnabled: true,
24226+
dnsConfig: &core.PodDNSConfig{Searches: []string{".."}},
24227+
},
24228+
{
24229+
name: "underscore and dot, featuregate enabled",
24230+
expectError: true,
24231+
featureEnabled: true,
24232+
dnsConfig: &core.PodDNSConfig{Searches: []string{"_."}},
24233+
},
24234+
{
24235+
name: "dash and dot, featuregate enabled",
24236+
expectError: true,
24237+
featureEnabled: true,
24238+
dnsConfig: &core.PodDNSConfig{Searches: []string{"-."}},
24239+
},
24240+
{
24241+
name: "two underscore and dot, featuregate enabled",
24242+
expectError: true,
24243+
featureEnabled: true,
24244+
dnsConfig: &core.PodDNSConfig{Searches: []string{"__."}},
24245+
},
24246+
{
24247+
name: "dot and two underscore, featuregate enabled",
24248+
expectError: true,
24249+
featureEnabled: true,
24250+
dnsConfig: &core.PodDNSConfig{Searches: []string{".__"}},
24251+
},
24252+
{
24253+
name: "dot and underscore, featuregate enabled",
24254+
expectError: true,
24255+
featureEnabled: true,
24256+
dnsConfig: &core.PodDNSConfig{Searches: []string{"._"}},
24257+
},
24258+
{
24259+
name: "lot of underscores, featuregate enabled",
24260+
expectError: true,
24261+
featureEnabled: true,
24262+
dnsConfig: &core.PodDNSConfig{Searches: []string{"____________"}},
24263+
},
24264+
{
24265+
name: "a regular name, featuregate enabled",
24266+
expectError: false,
24267+
featureEnabled: true,
24268+
dnsConfig: &core.PodDNSConfig{Searches: []string{"example.com"}},
24269+
},
24270+
{
24271+
name: "unicode character, featuregate enabled",
24272+
expectError: true,
24273+
featureEnabled: true,
24274+
dnsConfig: &core.PodDNSConfig{Searches: []string{"☃.example.com"}},
24275+
},
24276+
{
24277+
name: "begins with underscore, contains underscore, featuregate disabled",
24278+
expectError: true,
24279+
featureEnabled: false,
24280+
dnsConfig: &core.PodDNSConfig{Searches: []string{"_sip._tcp.abc_d.example.com"}},
24281+
},
24282+
{
24283+
name: "contains underscore, featuregate disabled",
24284+
expectError: true,
24285+
featureEnabled: false,
24286+
dnsConfig: &core.PodDNSConfig{Searches: []string{"abc_d.example.com"}},
24287+
},
24288+
{
24289+
name: "is dot, featuregate disabled",
24290+
expectError: true,
24291+
featureEnabled: false,
24292+
dnsConfig: &core.PodDNSConfig{Searches: []string{"."}},
24293+
},
24294+
{
24295+
name: "two dots, featuregate disabled",
24296+
expectError: true,
24297+
featureEnabled: false,
24298+
dnsConfig: &core.PodDNSConfig{Searches: []string{".."}},
24299+
},
24300+
{
24301+
name: "underscore and dot, featuregate disabled",
24302+
expectError: true,
24303+
featureEnabled: false,
24304+
dnsConfig: &core.PodDNSConfig{Searches: []string{"_."}},
24305+
},
24306+
{
24307+
name: "dash and dot, featuregate disabled",
24308+
expectError: true,
24309+
featureEnabled: false,
24310+
dnsConfig: &core.PodDNSConfig{Searches: []string{"-."}},
24311+
},
24312+
{
24313+
name: "two underscore and dot, featuregate disabled",
24314+
expectError: true,
24315+
featureEnabled: false,
24316+
dnsConfig: &core.PodDNSConfig{Searches: []string{"__."}},
24317+
},
24318+
{
24319+
name: "dot and two underscore, featuregate disabled",
24320+
expectError: true,
24321+
featureEnabled: false,
24322+
dnsConfig: &core.PodDNSConfig{Searches: []string{".__"}},
24323+
},
24324+
{
24325+
name: "dot and underscore, featuregate disabled",
24326+
expectError: true,
24327+
featureEnabled: false,
24328+
dnsConfig: &core.PodDNSConfig{Searches: []string{"._"}},
24329+
},
24330+
{
24331+
name: "lot of underscores, featuregate disabled",
24332+
expectError: true,
24333+
featureEnabled: false,
24334+
dnsConfig: &core.PodDNSConfig{Searches: []string{"____________"}},
24335+
},
24336+
{
24337+
name: "a regular name, featuregate disabled",
24338+
expectError: false,
24339+
featureEnabled: false,
24340+
dnsConfig: &core.PodDNSConfig{Searches: []string{"example.com"}},
24341+
},
24342+
{
24343+
name: "unicode character, featuregate disabled",
24344+
expectError: true,
24345+
featureEnabled: false,
24346+
dnsConfig: &core.PodDNSConfig{Searches: []string{"☃.example.com"}},
24347+
},
24348+
}
24349+
for _, testCase := range testCases {
24350+
t.Run(testCase.name, func(t *testing.T) {
24351+
errs := validatePodDNSConfig(testCase.dnsConfig, nil, nil, PodValidationOptions{AllowRelaxedDNSSearchValidation: testCase.featureEnabled})
24352+
if testCase.expectError && len(errs) == 0 {
24353+
t.Errorf("Unexpected success")
24354+
}
24355+
if !testCase.expectError && len(errs) != 0 {
24356+
t.Errorf("Unexpected error(s): %v", errs)
24357+
}
24358+
})
24359+
}
24360+
}
24361+
2419624362
// TODO: merge these test to TestValidatePodSpec after SupplementalGroupsPolicy feature graduates to Beta
2419724363
func TestValidatePodSpecWithSupplementalGroupsPolicy(t *testing.T) {
2419824364
fldPath := field.NewPath("spec")

pkg/features/kube_features.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,13 @@ const (
618618
// Allow users to recover from volume expansion failure
619619
RecoverVolumeExpansionFailure featuregate.Feature = "RecoverVolumeExpansionFailure"
620620

621+
// owner: @adrianmoisey
622+
// kep: https://kep.k8s.io/4427
623+
// alpha: v1.32
624+
//
625+
// Relaxed DNS search string validation.
626+
RelaxedDNSSearchValidation featuregate.Feature = "RelaxedDNSSearchValidation"
627+
621628
// owner: @HirazawaUi
622629
// kep: https://kep.k8s.io/4369
623630
// alpha: v1.30

pkg/features/versioned_kube_features.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,9 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
292292
{Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha},
293293
{Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.Beta},
294294
},
295-
295+
RelaxedDNSSearchValidation: {
296+
{Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha},
297+
},
296298
RelaxedEnvironmentVariableValidation: {
297299
{Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Alpha},
298300
},

staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ func IsValidLabelValue(value string) []string {
175175
}
176176

177177
const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
178+
const dns1123LabelFmtWithUnderscore string = "_?[a-z0-9]([-_a-z0-9]*[a-z0-9])?"
179+
178180
const dns1123LabelErrMsg string = "a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character"
179181

180182
// DNS1123LabelMaxLength is a label's max length in DNS (RFC 1123)
@@ -204,10 +206,14 @@ func IsDNS1123Label(value string) []string {
204206
const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
205207
const dns1123SubdomainErrorMsg string = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"
206208

209+
const dns1123SubdomainFmtWithUnderscore string = dns1123LabelFmtWithUnderscore + "(\\." + dns1123LabelFmtWithUnderscore + ")*"
210+
const dns1123SubdomainErrorMsgFG string = "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '_', '-' or '.', and must start and end with an alphanumeric character"
211+
207212
// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123)
208213
const DNS1123SubdomainMaxLength int = 253
209214

210215
var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
216+
var dns1123SubdomainRegexpWithUnderscore = regexp.MustCompile("^" + dns1123SubdomainFmtWithUnderscore + "$")
211217

212218
// IsDNS1123Subdomain tests for a string that conforms to the definition of a
213219
// subdomain in DNS (RFC 1123).
@@ -222,6 +228,19 @@ func IsDNS1123Subdomain(value string) []string {
222228
return errs
223229
}
224230

231+
// IsDNS1123SubdomainWithUnderscore tests for a string that conforms to the definition of a
232+
// subdomain in DNS (RFC 1123), but allows the use of an underscore in the string
233+
func IsDNS1123SubdomainWithUnderscore(value string) []string {
234+
var errs []string
235+
if len(value) > DNS1123SubdomainMaxLength {
236+
errs = append(errs, MaxLenError(DNS1123SubdomainMaxLength))
237+
}
238+
if !dns1123SubdomainRegexpWithUnderscore.MatchString(value) {
239+
errs = append(errs, RegexError(dns1123SubdomainErrorMsgFG, dns1123SubdomainFmt, "example.com"))
240+
}
241+
return errs
242+
}
243+
225244
const dns1035LabelFmt string = "[a-z]([-a-z0-9]*[a-z0-9])?"
226245
const dns1035LabelErrMsg string = "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"
227246

test/e2e/feature/feature.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ var (
283283
// and whether the pod can consume configmap/secret that key starts with a number.
284284
RelaxedEnvironmentVariableValidation = framework.WithFeature(framework.ValidFeatures.Add("RelaxedEnvironmentVariableValidation"))
285285

286+
// Owner: sig-network
287+
// Marks tests of KEP-4427 that require the `RelaxedDNSSearchValidation` feature gate
288+
RelaxedDNSSearchValidation = framework.WithFeature(framework.ValidFeatures.Add("RelaxedDNSSearchValidation"))
289+
286290
// TODO: document the feature (owning SIG, when to use this feature for a test)
287291
Recreate = framework.WithFeature(framework.ValidFeatures.Add("Recreate"))
288292

test/e2e/network/dns.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
v1 "k8s.io/api/core/v1"
2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2727
"k8s.io/apimachinery/pkg/util/wait"
28+
"k8s.io/kubernetes/test/e2e/feature"
2829
"k8s.io/kubernetes/test/e2e/framework"
2930
e2enode "k8s.io/kubernetes/test/e2e/framework/node"
3031
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
@@ -597,7 +598,39 @@ var _ = common.SIGDescribe("DNS", func() {
597598
}
598599
validateDNSResults(ctx, f, pod, append(wheezyFileNames, jessieFileNames...))
599600
})
601+
})
602+
603+
// TODO replace WithLabel by framework.WithFeatureGate(features.RelaxedDNSSearchValidation) once https://github.com/kubernetes/kubernetes/pull/126977 is solved
604+
var _ = common.SIGDescribe("DNS", feature.RelaxedDNSSearchValidation, framework.WithLabel("Feature:Alpha"), func() {
605+
f := framework.NewDefaultFramework("dns")
606+
f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
600607

608+
ginkgo.It("should work with a search path containing an underscore and a search path with a single dot", func(ctx context.Context) {
609+
// All the names we need to be able to resolve.
610+
namesToResolve := []string{
611+
"kubernetes.default",
612+
"kubernetes.default.svc",
613+
}
614+
hostFQDN := fmt.Sprintf("%s.%s.%s.svc.%s", dnsTestPodHostName, dnsTestServiceName, f.Namespace.Name, framework.TestContext.ClusterDNSDomain)
615+
hostEntries := []string{hostFQDN, dnsTestPodHostName}
616+
// TODO: Validate both IPv4 and IPv6 families for dual-stack
617+
wheezyProbeCmd, wheezyFileNames := createProbeCommand(namesToResolve, hostEntries, "", "wheezy", f.Namespace.Name, framework.TestContext.ClusterDNSDomain, framework.TestContext.ClusterIsIPv6())
618+
jessieProbeCmd, jessieFileNames := createProbeCommand(namesToResolve, hostEntries, "", "jessie", f.Namespace.Name, framework.TestContext.ClusterDNSDomain, framework.TestContext.ClusterIsIPv6())
619+
ginkgo.By("Running these commands on wheezy: " + wheezyProbeCmd + "\n")
620+
ginkgo.By("Running these commands on jessie: " + jessieProbeCmd + "\n")
621+
622+
ginkgo.By("Creating a pod with expanded DNS configuration to probe DNS")
623+
testSearchPaths := []string{
624+
".",
625+
"_sip._tcp.abc_d.example.com",
626+
}
627+
pod := createDNSPod(f.Namespace.Name, wheezyProbeCmd, jessieProbeCmd, dnsTestPodHostName, dnsTestServiceName)
628+
pod.Spec.DNSPolicy = v1.DNSClusterFirst
629+
pod.Spec.DNSConfig = &v1.PodDNSConfig{
630+
Searches: testSearchPaths,
631+
}
632+
validateDNSResults(ctx, f, pod, append(wheezyFileNames, jessieFileNames...))
633+
})
601634
})
602635

603636
var _ = common.SIGDescribe("DNS HostNetwork", func() {

0 commit comments

Comments
 (0)