diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index aaccd4d2ca4..5eb0fb12206 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -2507,16 +2507,81 @@ func validateDNSPolicy(dnsPolicy *core.DNSPolicy, fldPath *field.Path) field.Err allErrors := field.ErrorList{} switch *dnsPolicy { case core.DNSClusterFirstWithHostNet, core.DNSClusterFirst, core.DNSDefault: - break + case core.DNSNone: + if !utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) { + allErrors = append(allErrors, field.Invalid(fldPath, dnsPolicy, "DNSPolicy: can not use 'None', custom pod DNS is disabled by feature gate")) + } case "": allErrors = append(allErrors, field.Required(fldPath, "")) default: validValues := []string{string(core.DNSClusterFirstWithHostNet), string(core.DNSClusterFirst), string(core.DNSDefault)} + if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) { + validValues = append(validValues, string(core.DNSNone)) + } allErrors = append(allErrors, field.NotSupported(fldPath, dnsPolicy, validValues)) } return allErrors } +const ( + // Limits on various DNS parameters. These are derived from + // restrictions in Linux libc name resolution handling. + // Max number of DNS name servers. + MaxDNSNameservers = 3 + // Max number of domains in search path. + MaxDNSSearchPaths = 6 + // Max number of characters in search path. + MaxDNSSearchListChars = 256 +) + +func validatePodDNSConfig(dnsConfig *core.PodDNSConfig, dnsPolicy *core.DNSPolicy, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // Validate DNSNone case. Must provide at least one DNS name server. + if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) && dnsPolicy != nil && *dnsPolicy == core.DNSNone { + if dnsConfig == nil { + return append(allErrs, field.Required(fldPath, fmt.Sprintf("must provide `dnsConfig` when `dnsPolicy` is %s", core.DNSNone))) + } + if len(dnsConfig.Nameservers) == 0 { + return append(allErrs, field.Required(fldPath.Child("nameservers"), fmt.Sprintf("must provide at least one DNS nameserver when `dnsPolicy` is %s", core.DNSNone))) + } + } + + if dnsConfig != nil { + if !utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) { + return append(allErrs, field.Forbidden(fldPath, "DNSConfig: custom pod DNS is disabled by feature gate")) + } + + // Validate nameservers. + if len(dnsConfig.Nameservers) > MaxDNSNameservers { + allErrs = append(allErrs, field.Invalid(fldPath.Child("nameservers"), dnsConfig.Nameservers, fmt.Sprintf("must not have more than %v nameservers", MaxDNSNameservers))) + } + for i, ns := range dnsConfig.Nameservers { + if ip := net.ParseIP(ns); ip == nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("nameservers").Index(i), ns, "must be valid IP address")) + } + } + // Validate searches. + if len(dnsConfig.Searches) > MaxDNSSearchPaths { + allErrs = append(allErrs, field.Invalid(fldPath.Child("searches"), dnsConfig.Searches, fmt.Sprintf("must not have more than %v search paths", MaxDNSSearchPaths))) + } + // Include the space between search paths. + if len(strings.Join(dnsConfig.Searches, " ")) > MaxDNSSearchListChars { + allErrs = append(allErrs, field.Invalid(fldPath.Child("searches"), dnsConfig.Searches, "must not have more than 256 characters (including spaces) in the search list")) + } + for i, search := range dnsConfig.Searches { + allErrs = append(allErrs, ValidateDNS1123Subdomain(search, fldPath.Child("searches").Index(i))...) + } + // Validate options. + for i, option := range dnsConfig.Options { + if len(option.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("options").Index(i), "must not be empty")) + } + } + } + return allErrs +} + func validateHostNetwork(hostNetwork bool, containers []core.Container, fldPath *field.Path) field.ErrorList { allErrors := field.ErrorList{} if hostNetwork { @@ -2767,6 +2832,7 @@ func ValidatePodSpec(spec *core.PodSpec, fldPath *field.Path) field.ErrorList { allErrs = append(allErrs, ValidatePodSecurityContext(spec.SecurityContext, spec, fldPath, fldPath.Child("securityContext"))...) allErrs = append(allErrs, validateImagePullSecrets(spec.ImagePullSecrets, fldPath.Child("imagePullSecrets"))...) allErrs = append(allErrs, validateAffinity(spec.Affinity, fldPath.Child("affinity"))...) + allErrs = append(allErrs, validatePodDNSConfig(spec.DNSConfig, &spec.DNSPolicy, fldPath.Child("dnsConfig"))...) if len(spec.ServiceAccountName) > 0 { for _, msg := range ValidateServiceAccountName(spec.ServiceAccountName, false) { allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceAccountName"), spec.ServiceAccountName, msg)) diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 5583eb9a857..62c4c3d0597 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package validation import ( + "fmt" "math" "reflect" "strings" @@ -5148,7 +5149,18 @@ func TestValidateRestartPolicy(t *testing.T) { } func TestValidateDNSPolicy(t *testing.T) { - successCases := []core.DNSPolicy{core.DNSClusterFirst, core.DNSDefault, core.DNSPolicy(core.DNSClusterFirst)} + customDNSEnabled := utilfeature.DefaultFeatureGate.Enabled("CustomPodDNS") + defer func() { + // Restoring the old value. + if err := utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("CustomPodDNS=%v", customDNSEnabled)); err != nil { + t.Errorf("Failed to restore CustomPodDNS feature gate: %v", err) + } + }() + if err := utilfeature.DefaultFeatureGate.Set("CustomPodDNS=true"); err != nil { + t.Errorf("Failed to enable CustomPodDNS feature gate: %v", err) + } + + successCases := []core.DNSPolicy{core.DNSClusterFirst, core.DNSDefault, core.DNSPolicy(core.DNSClusterFirst), core.DNSNone} for _, policy := range successCases { if errs := validateDNSPolicy(&policy, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) @@ -5163,6 +5175,177 @@ func TestValidateDNSPolicy(t *testing.T) { } } +func TestValidatePodDNSConfig(t *testing.T) { + customDNSEnabled := utilfeature.DefaultFeatureGate.Enabled("CustomPodDNS") + defer func() { + // Restoring the old value. + if err := utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("CustomPodDNS=%v", customDNSEnabled)); err != nil { + t.Errorf("Failed to restore CustomPodDNS feature gate: %v", err) + } + }() + if err := utilfeature.DefaultFeatureGate.Set("CustomPodDNS=true"); err != nil { + t.Errorf("Failed to enable CustomPodDNS feature gate: %v", err) + } + + generateTestSearchPathFunc := func(numChars int) string { + res := "" + for i := 0; i < numChars; i++ { + res = res + "a" + } + return res + } + testOptionValue := "2" + testDNSNone := core.DNSNone + testDNSClusterFirst := core.DNSClusterFirst + + testCases := []struct { + desc string + dnsConfig *core.PodDNSConfig + dnsPolicy *core.DNSPolicy + expectedError bool + }{ + { + desc: "valid: empty DNSConfig", + dnsConfig: &core.PodDNSConfig{}, + expectedError: false, + }, + { + desc: "valid: 1 option", + dnsConfig: &core.PodDNSConfig{ + Options: []core.PodDNSConfigOption{ + {Name: "ndots", Value: &testOptionValue}, + }, + }, + expectedError: false, + }, + { + desc: "valid: 1 nameserver", + dnsConfig: &core.PodDNSConfig{ + Nameservers: []string{"127.0.0.1"}, + }, + expectedError: false, + }, + { + desc: "valid: DNSNone with 1 nameserver", + dnsConfig: &core.PodDNSConfig{ + Nameservers: []string{"127.0.0.1"}, + }, + dnsPolicy: &testDNSNone, + expectedError: false, + }, + { + desc: "valid: 1 search path", + dnsConfig: &core.PodDNSConfig{ + Searches: []string{"custom"}, + }, + expectedError: false, + }, + { + desc: "valid: 3 nameservers and 6 search paths", + dnsConfig: &core.PodDNSConfig{ + Nameservers: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8"}, + Searches: []string{"custom", "mydomain.com", "local", "cluster.local", "svc.cluster.local", "default.svc.cluster.local"}, + }, + expectedError: false, + }, + { + desc: "valid: 256 characters in search path list", + dnsConfig: &core.PodDNSConfig{ + // We can have 256 - (6 - 1) = 251 characters in total for 6 search paths. + Searches: []string{ + generateTestSearchPathFunc(1), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + }, + }, + expectedError: false, + }, + { + desc: "valid: ipv6 nameserver", + dnsConfig: &core.PodDNSConfig{ + Nameservers: []string{"FE80::0202:B3FF:FE1E:8329"}, + }, + expectedError: false, + }, + { + desc: "invalid: 4 nameservers", + dnsConfig: &core.PodDNSConfig{ + Nameservers: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8", "1.2.3.4"}, + }, + expectedError: true, + }, + { + desc: "invalid: 7 search paths", + dnsConfig: &core.PodDNSConfig{ + Searches: []string{"custom", "mydomain.com", "local", "cluster.local", "svc.cluster.local", "default.svc.cluster.local", "exceeded"}, + }, + expectedError: true, + }, + { + desc: "invalid: 257 characters in search path list", + dnsConfig: &core.PodDNSConfig{ + // We can have 256 - (6 - 1) = 251 characters in total for 6 search paths. + Searches: []string{ + generateTestSearchPathFunc(2), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + generateTestSearchPathFunc(50), + }, + }, + expectedError: true, + }, + { + desc: "invalid search path", + dnsConfig: &core.PodDNSConfig{ + Searches: []string{"custom?"}, + }, + expectedError: true, + }, + { + desc: "invalid nameserver", + dnsConfig: &core.PodDNSConfig{ + Nameservers: []string{"invalid"}, + }, + expectedError: true, + }, + { + desc: "invalid empty option name", + dnsConfig: &core.PodDNSConfig{ + Options: []core.PodDNSConfigOption{ + {Value: &testOptionValue}, + }, + }, + expectedError: true, + }, + { + desc: "invalid: DNSNone with 0 nameserver", + dnsConfig: &core.PodDNSConfig{ + Searches: []string{"custom"}, + }, + dnsPolicy: &testDNSNone, + expectedError: true, + }, + } + + for _, tc := range testCases { + if tc.dnsPolicy == nil { + tc.dnsPolicy = &testDNSClusterFirst + } + + errs := validatePodDNSConfig(tc.dnsConfig, tc.dnsPolicy, field.NewPath("dnsConfig")) + if len(errs) != 0 && !tc.expectedError { + t.Errorf("%v: validatePodDNSConfig(%v) = %v, want nil", tc.desc, tc.dnsConfig, errs) + } else if len(errs) == 0 && tc.expectedError { + t.Errorf("%v: validatePodDNSConfig(%v) = nil, want error", tc.desc, tc.dnsConfig) + } + } +} + func TestValidatePodSpec(t *testing.T) { activeDeadlineSeconds := int64(30) activeDeadlineSecondsMax := int64(math.MaxInt32)