diff --git a/pkg/kubelet/network/dns/BUILD b/pkg/kubelet/network/dns/BUILD index 0f18f442e76..c0a4c05463a 100644 --- a/pkg/kubelet/network/dns/BUILD +++ b/pkg/kubelet/network/dns/BUILD @@ -6,11 +6,14 @@ go_library( importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns", visibility = ["//visibility:public"], deps = [ + "//pkg/apis/core/validation:go_default_library", + "//pkg/features:go_default_library", "//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library", "//pkg/kubelet/container:go_default_library", "//pkg/kubelet/util/format:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/client-go/tools/record:go_default_library", ], ) @@ -21,11 +24,14 @@ go_test( importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns", library = ":go_default_library", deps = [ + "//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/k8s.io/client-go/tools/record:go_default_library", ], ) diff --git a/pkg/kubelet/network/dns/dns.go b/pkg/kubelet/network/dns/dns.go index d37661c725a..bcc717e01c1 100644 --- a/pkg/kubelet/network/dns/dns.go +++ b/pkg/kubelet/network/dns/dns.go @@ -26,7 +26,10 @@ import ( "strings" "k8s.io/api/core/v1" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/record" + "k8s.io/kubernetes/pkg/apis/core/validation" + "k8s.io/kubernetes/pkg/features" runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" "k8s.io/kubernetes/pkg/kubelet/util/format" @@ -39,6 +42,14 @@ var ( defaultDNSOptions = []string{"ndots:5"} ) +type podDNSType int + +const ( + podDNSCluster podDNSType = iota + podDNSHost + podDNSNone +) + // Configurer is used for setting up DNS resolver configuration when launching pods. type Configurer struct { recorder record.EventRecorder @@ -67,37 +78,35 @@ func NewConfigurer(recorder record.EventRecorder, nodeRef *v1.ObjectReference, n } } -func omitDuplicates(pod *v1.Pod, combinedSearch []string) []string { - uniqueDomains := map[string]bool{} +func omitDuplicates(strs []string) []string { + uniqueStrs := make(map[string]bool) - for _, dnsDomain := range combinedSearch { - if _, exists := uniqueDomains[dnsDomain]; !exists { - combinedSearch[len(uniqueDomains)] = dnsDomain - uniqueDomains[dnsDomain] = true + var ret []string + for _, str := range strs { + if !uniqueStrs[str] { + ret = append(ret, str) + uniqueStrs[str] = true } } - return combinedSearch[:len(uniqueDomains)] + return ret } -func (c *Configurer) formDNSSearchFitsLimits(pod *v1.Pod, composedSearch []string) []string { - // resolver file Search line current limitations - resolvSearchLineDNSDomainsLimit := 6 - resolvSearchLineLenLimit := 255 +func (c *Configurer) formDNSSearchFitsLimits(composedSearch []string, pod *v1.Pod) []string { limitsExceeded := false - if len(composedSearch) > resolvSearchLineDNSDomainsLimit { - composedSearch = composedSearch[:resolvSearchLineDNSDomainsLimit] + if len(composedSearch) > validation.MaxDNSSearchPaths { + composedSearch = composedSearch[:validation.MaxDNSSearchPaths] limitsExceeded = true } - if resolvSearchhLineStrLen := len(strings.Join(composedSearch, " ")); resolvSearchhLineStrLen > resolvSearchLineLenLimit { + if resolvSearchLineStrLen := len(strings.Join(composedSearch, " ")); resolvSearchLineStrLen > validation.MaxDNSSearchListChars { cutDomainsNum := 0 - cutDoaminsLen := 0 + cutDomainsLen := 0 for i := len(composedSearch) - 1; i >= 0; i-- { - cutDoaminsLen += len(composedSearch[i]) + 1 + cutDomainsLen += len(composedSearch[i]) + 1 cutDomainsNum++ - if (resolvSearchhLineStrLen - cutDoaminsLen) <= resolvSearchLineLenLimit { + if (resolvSearchLineStrLen - cutDomainsLen) <= validation.MaxDNSSearchListChars { break } } @@ -107,39 +116,43 @@ func (c *Configurer) formDNSSearchFitsLimits(pod *v1.Pod, composedSearch []strin } if limitsExceeded { - log := fmt.Sprintf("Search Line limits were exceeded, some dns names have been omitted, the applied search line is: %s", strings.Join(composedSearch, " ")) - c.recorder.Event(pod, v1.EventTypeWarning, "DNSSearchForming", log) + log := fmt.Sprintf("Search Line limits were exceeded, some search paths have been omitted, the applied search line is: %s", strings.Join(composedSearch, " ")) + c.recorder.Event(pod, v1.EventTypeWarning, "DNSConfigForming", log) glog.Error(log) } return composedSearch } -func (c *Configurer) formDNSSearchForDNSDefault(hostSearch []string, pod *v1.Pod) []string { - return c.formDNSSearchFitsLimits(pod, hostSearch) +func (c *Configurer) formDNSNameserversFitsLimits(nameservers []string, pod *v1.Pod) []string { + if len(nameservers) > validation.MaxDNSNameservers { + nameservers = nameservers[0:validation.MaxDNSNameservers] + log := fmt.Sprintf("Nameserver limits were exceeded, some nameservers have been omitted, the applied nameserver line is: %s", strings.Join(nameservers, " ")) + c.recorder.Event(pod, v1.EventTypeWarning, "DNSConfigForming", log) + glog.Error(log) + } + return nameservers } -func (c *Configurer) formDNSSearch(hostSearch []string, pod *v1.Pod) []string { +func (c *Configurer) formDNSConfigFitsLimits(dnsConfig *runtimeapi.DNSConfig, pod *v1.Pod) *runtimeapi.DNSConfig { + dnsConfig.Servers = c.formDNSNameserversFitsLimits(dnsConfig.Servers, pod) + dnsConfig.Searches = c.formDNSSearchFitsLimits(dnsConfig.Searches, pod) + return dnsConfig +} + +func (c *Configurer) generateSearchesForDNSClusterFirst(hostSearch []string, pod *v1.Pod) []string { if c.ClusterDomain == "" { - c.formDNSSearchFitsLimits(pod, hostSearch) return hostSearch } nsSvcDomain := fmt.Sprintf("%s.svc.%s", pod.Namespace, c.ClusterDomain) svcDomain := fmt.Sprintf("svc.%s", c.ClusterDomain) - dnsSearch := []string{nsSvcDomain, svcDomain, c.ClusterDomain} + clusterSearch := []string{nsSvcDomain, svcDomain, c.ClusterDomain} - combinedSearch := append(dnsSearch, hostSearch...) - - combinedSearch = omitDuplicates(pod, combinedSearch) - return c.formDNSSearchFitsLimits(pod, combinedSearch) + return omitDuplicates(append(clusterSearch, hostSearch...)) } // CheckLimitsForResolvConf checks limits in resolv.conf. func (c *Configurer) CheckLimitsForResolvConf() { - // resolver file Search line current limitations - resolvSearchLineDNSDomainsLimit := 6 - resolvSearchLineLenLimit := 255 - f, err := os.Open(c.ResolverConfig) if err != nil { c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", err.Error()) @@ -155,21 +168,21 @@ func (c *Configurer) CheckLimitsForResolvConf() { return } - domainCntLimit := resolvSearchLineDNSDomainsLimit + domainCountLimit := validation.MaxDNSSearchPaths if c.ClusterDomain != "" { - domainCntLimit -= 3 + domainCountLimit -= 3 } - if len(hostSearch) > domainCntLimit { - log := fmt.Sprintf("Resolv.conf file '%s' contains search line consisting of more than %d domains!", c.ResolverConfig, domainCntLimit) + if len(hostSearch) > domainCountLimit { + log := fmt.Sprintf("Resolv.conf file '%s' contains search line consisting of more than %d domains!", c.ResolverConfig, domainCountLimit) c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", log) glog.Error("CheckLimitsForResolvConf: " + log) return } - if len(strings.Join(hostSearch, " ")) > resolvSearchLineLenLimit { - log := fmt.Sprintf("Resolv.conf file '%s' contains search line which length is more than allowed %d chars!", c.ResolverConfig, resolvSearchLineLenLimit) + if len(strings.Join(hostSearch, " ")) > validation.MaxDNSSearchListChars { + log := fmt.Sprintf("Resolv.conf file '%s' contains search line which length is more than allowed %d chars!", c.ResolverConfig, validation.MaxDNSSearchListChars) c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", log) glog.Error("CheckLimitsForResolvConf: " + log) return @@ -180,7 +193,6 @@ func (c *Configurer) CheckLimitsForResolvConf() { // parseResolveConf reads a resolv.conf file from the given reader, and parses // it into nameservers, searches and options, possibly returning an error. -// TODO: move to utility package func parseResolvConf(reader io.Reader) (nameservers []string, searches []string, options []string, err error) { file, err := ioutil.ReadAll(reader) if err != nil { @@ -218,15 +230,10 @@ func parseResolvConf(reader io.Reader) (nameservers []string, searches []string, } } - // There used to be code here to scrub DNS for each cloud, but doesn't - // make sense anymore since cloudproviders are being factored out. - // contact @thockin or @wlan0 for more information - return nameservers, searches, options, nil } -// GetPodDNS returns DNS setttings for the pod. -func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) { +func (c *Configurer) getHostDNSConfig(pod *v1.Pod) (*runtimeapi.DNSConfig, error) { var hostDNS, hostSearch, hostOptions []string // Get host DNS settings if c.ResolverConfig != "" { @@ -241,19 +248,117 @@ func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) { return nil, err } } - useClusterFirstPolicy := ((pod.Spec.DNSPolicy == v1.DNSClusterFirst && !kubecontainer.IsHostNetworkPod(pod)) || pod.Spec.DNSPolicy == v1.DNSClusterFirstWithHostNet) - if useClusterFirstPolicy && len(c.clusterDNS) == 0 { - // clusterDNS is not known. - // pod with ClusterDNSFirst Policy cannot be created - c.recorder.Eventf(pod, v1.EventTypeWarning, "MissingClusterDNS", "kubelet does not have ClusterDNS IP configured and cannot create Pod using %q policy. Falling back to DNSDefault policy.", pod.Spec.DNSPolicy) - log := fmt.Sprintf("kubelet does not have ClusterDNS IP configured and cannot create Pod using %q policy. pod: %q. Falling back to DNSDefault policy.", pod.Spec.DNSPolicy, format.Pod(pod)) - c.recorder.Eventf(c.nodeRef, v1.EventTypeWarning, "MissingClusterDNS", log) + return &runtimeapi.DNSConfig{ + Servers: hostDNS, + Searches: hostSearch, + Options: hostOptions, + }, nil +} - // fallback to DNSDefault - useClusterFirstPolicy = false +func getPodDNSType(pod *v1.Pod) (podDNSType, error) { + dnsPolicy := pod.Spec.DNSPolicy + switch dnsPolicy { + case v1.DNSNone: + if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) { + return podDNSNone, nil + } + // This should not happen as kube-apiserver should have rejected + // setting dnsPolicy to DNSNone when feature gate is disabled. + return podDNSCluster, fmt.Errorf(fmt.Sprintf("invalid DNSPolicy=%v: custom pod DNS is disabled", dnsPolicy)) + case v1.DNSClusterFirstWithHostNet: + return podDNSCluster, nil + case v1.DNSClusterFirst: + if !kubecontainer.IsHostNetworkPod(pod) { + return podDNSCluster, nil + } + // Fallback to DNSDefault for pod on hostnetowrk. + fallthrough + case v1.DNSDefault: + return podDNSHost, nil + } + // This should not happen as kube-apiserver should have rejected + // invalid dnsPolicy. + return podDNSCluster, fmt.Errorf(fmt.Sprintf("invalid DNSPolicy=%v", dnsPolicy)) +} + +// Merge DNS options. If duplicated, entries given by PodDNSConfigOption will +// overwrite the existing ones. +func mergeDNSOptions(existingDNSConfigOptions []string, dnsConfigOptions []v1.PodDNSConfigOption) []string { + optionsMap := make(map[string]string) + for _, op := range existingDNSConfigOptions { + if index := strings.Index(op, ":"); index != -1 { + optionsMap[op[:index]] = op[index+1:] + } else { + optionsMap[op] = "" + } + } + for _, op := range dnsConfigOptions { + if op.Value != nil { + optionsMap[op.Name] = *op.Value + } else { + optionsMap[op.Name] = "" + } + } + // Reconvert DNS options into a string array. + options := []string{} + for opName, opValue := range optionsMap { + op := opName + if opValue != "" { + op = op + ":" + opValue + } + options = append(options, op) + } + return options +} + +// appendDNSConfig appends DNS servers, search paths and options given by +// PodDNSConfig to the existing DNS config. Duplicated entries will be merged. +// This assumes existingDNSConfig and dnsConfig are not nil. +func appendDNSConfig(existingDNSConfig *runtimeapi.DNSConfig, dnsConfig *v1.PodDNSConfig) *runtimeapi.DNSConfig { + existingDNSConfig.Servers = omitDuplicates(append(existingDNSConfig.Servers, dnsConfig.Nameservers...)) + existingDNSConfig.Searches = omitDuplicates(append(existingDNSConfig.Searches, dnsConfig.Searches...)) + existingDNSConfig.Options = mergeDNSOptions(existingDNSConfig.Options, dnsConfig.Options) + return existingDNSConfig +} + +// GetPodDNS returns DNS setttings for the pod. +func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) { + dnsConfig, err := c.getHostDNSConfig(pod) + if err != nil { + return nil, err } - if !useClusterFirstPolicy { + dnsType, err := getPodDNSType(pod) + if err != nil { + glog.Errorf("Failed to get DNS type for pod %q: %v. Falling back to DNSClusterFirst policy.", format.Pod(pod), err) + dnsType = podDNSCluster + } + switch dnsType { + case podDNSNone: + // DNSNone should use empty DNS settings as the base. + dnsConfig = &runtimeapi.DNSConfig{} + case podDNSCluster: + if len(c.clusterDNS) != 0 { + // For a pod with DNSClusterFirst policy, the cluster DNS server is + // the only nameserver configured for the pod. The cluster DNS server + // itself will forward queries to other nameservers that is configured + // to use, in case the cluster DNS server cannot resolve the DNS query + // itself. + dnsConfig.Servers = []string{} + for _, ip := range c.clusterDNS { + dnsConfig.Servers = append(dnsConfig.Servers, ip.String()) + } + dnsConfig.Searches = c.generateSearchesForDNSClusterFirst(dnsConfig.Searches, pod) + dnsConfig.Options = defaultDNSOptions + break + } + // clusterDNS is not known. Pod with ClusterDNSFirst Policy cannot be created. + nodeErrorMsg := fmt.Sprintf("kubelet does not have ClusterDNS IP configured and cannot create Pod using %q policy. Falling back to %q policy.", v1.DNSClusterFirst, v1.DNSDefault) + c.recorder.Eventf(c.nodeRef, v1.EventTypeWarning, "MissingClusterDNS", nodeErrorMsg) + c.recorder.Eventf(pod, v1.EventTypeWarning, "MissingClusterDNS", "pod: %q. %s", format.Pod(pod), nodeErrorMsg) + // Fallback to DNSDefault. + fallthrough + case podDNSHost: // When the kubelet --resolv-conf flag is set to the empty string, use // DNS settings that override the docker default (which is to use // /etc/resolv.conf) and effectively disable DNS lookups. According to @@ -262,35 +367,20 @@ func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) { // local machine". A nameserver setting of localhost is equivalent to // this documented behavior. if c.ResolverConfig == "" { - hostSearch = []string{"."} switch { case c.nodeIP == nil || c.nodeIP.To4() != nil: - hostDNS = []string{"127.0.0.1"} + dnsConfig.Servers = []string{"127.0.0.1"} case c.nodeIP.To16() != nil: - hostDNS = []string{"::1"} + dnsConfig.Servers = []string{"::1"} } - } else { - hostSearch = c.formDNSSearchForDNSDefault(hostSearch, pod) + dnsConfig.Searches = []string{"."} } - return &runtimeapi.DNSConfig{ - Servers: hostDNS, - Searches: hostSearch, - Options: hostOptions}, nil } - // for a pod with DNSClusterFirst policy, the cluster DNS server is the only nameserver configured for - // the pod. The cluster DNS server itself will forward queries to other nameservers that is configured to use, - // in case the cluster DNS server cannot resolve the DNS query itself - dns := make([]string, len(c.clusterDNS)) - for i, ip := range c.clusterDNS { - dns[i] = ip.String() + if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) && pod.Spec.DNSConfig != nil { + dnsConfig = appendDNSConfig(dnsConfig, pod.Spec.DNSConfig) } - dnsSearch := c.formDNSSearch(hostSearch, pod) - - return &runtimeapi.DNSConfig{ - Servers: dns, - Searches: dnsSearch, - Options: defaultDNSOptions}, nil + return c.formDNSConfigFitsLimits(dnsConfig, pod), nil } // SetupDNSinContainerizedMounter replaces the nameserver in containerized-mounter's rootfs/etc/resolve.conf with kubelet.ClusterDNS diff --git a/pkg/kubelet/network/dns/dns_test.go b/pkg/kubelet/network/dns/dns_test.go index 4a53f66cc5e..3ff551f4a6e 100644 --- a/pkg/kubelet/network/dns/dns_test.go +++ b/pkg/kubelet/network/dns/dns_test.go @@ -25,12 +25,26 @@ import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/record" + runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var ( + fetchEvent = func(recorder *record.FakeRecorder) string { + select { + case event := <-recorder.Events: + return event + default: + return "" + } + } +) + func TestParseResolvConf(t *testing.T) { testCases := []struct { data string @@ -74,7 +88,7 @@ func TestParseResolvConf(t *testing.T) { } } -func TestComposeDNSSearch(t *testing.T) { +func TestFormDNSSearchFitsLimits(t *testing.T) { recorder := record.NewFakeRecorder(20) nodeRef := &v1.ObjectReference{ Kind: "Node", @@ -96,62 +110,272 @@ func TestComposeDNSSearch(t *testing.T) { } testCases := []struct { - dnsNames []string hostNames []string resultSearch []string events []string }{ { []string{"testNS.svc.TEST", "svc.TEST", "TEST"}, - []string{}, []string{"testNS.svc.TEST", "svc.TEST", "TEST"}, []string{}, }, { - []string{"testNS.svc.TEST", "svc.TEST", "TEST"}, - []string{"AAA", "svc.TEST", "BBB", "TEST"}, + []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB"}, []string{}, }, { - []string{"testNS.svc.TEST", "svc.TEST", "TEST"}, - []string{"AAA", strings.Repeat("B", 256), "BBB"}, + []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", strings.Repeat("B", 256), "BBB"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA"}, - []string{"Search Line limits were exceeded, some dns names have been omitted, the applied search line is: testNS.svc.TEST svc.TEST TEST AAA"}, + []string{"Search Line limits were exceeded, some search paths have been omitted, the applied search line is: testNS.svc.TEST svc.TEST TEST AAA"}, }, { - []string{"testNS.svc.TEST", "svc.TEST", "TEST"}, - []string{"AAA", "TEST", "BBB", "TEST", "CCC", "DDD"}, + []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB", "CCC", "DDD"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB", "CCC"}, - []string{ - "Search Line limits were exceeded, some dns names have been omitted, the applied search line is: testNS.svc.TEST svc.TEST TEST AAA BBB CCC", - }, + []string{"Search Line limits were exceeded, some search paths have been omitted, the applied search line is: testNS.svc.TEST svc.TEST TEST AAA BBB CCC"}, }, } - fetchEvent := func(recorder *record.FakeRecorder) string { - select { - case event := <-recorder.Events: - return event - default: - return "No more events!" - } - } - for i, tc := range testCases { - dnsSearch := configurer.formDNSSearch(tc.hostNames, pod) + dnsSearch := configurer.formDNSSearchFitsLimits(tc.hostNames, pod) assert.EqualValues(t, tc.resultSearch, dnsSearch, "test [%d]", i) for _, expectedEvent := range tc.events { - expected := fmt.Sprintf("%s %s %s", v1.EventTypeWarning, "DNSSearchForming", expectedEvent) + expected := fmt.Sprintf("%s %s %s", v1.EventTypeWarning, "DNSConfigForming", expectedEvent) event := fetchEvent(recorder) assert.Equal(t, expected, event, "test [%d]", i) } } } +func TestFormDNSNameserversFitsLimits(t *testing.T) { + recorder := record.NewFakeRecorder(20) + nodeRef := &v1.ObjectReference{ + Kind: "Node", + Name: string("testNode"), + UID: types.UID("testNode"), + Namespace: "", + } + testClusterDNSDomain := "TEST" + + configurer := NewConfigurer(recorder, nodeRef, nil, nil, testClusterDNSDomain, "") + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: "", + Name: "test_pod", + Namespace: "testNS", + Annotations: map[string]string{}, + }, + } + + testCases := []struct { + desc string + nameservers []string + expectedNameserver []string + expectedEvent bool + }{ + { + desc: "valid: 1 nameserver", + nameservers: []string{"127.0.0.1"}, + expectedNameserver: []string{"127.0.0.1"}, + expectedEvent: false, + }, + { + desc: "valid: 3 nameservers", + nameservers: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8"}, + expectedNameserver: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8"}, + expectedEvent: false, + }, + { + desc: "invalid: 4 nameservers, trimmed to 3", + nameservers: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8", "1.2.3.4"}, + expectedNameserver: []string{"127.0.0.1", "10.0.0.10", "8.8.8.8"}, + expectedEvent: true, + }, + } + + for _, tc := range testCases { + appliedNameservers := configurer.formDNSNameserversFitsLimits(tc.nameservers, pod) + assert.EqualValues(t, tc.expectedNameserver, appliedNameservers, tc.desc) + event := fetchEvent(recorder) + if tc.expectedEvent && len(event) == 0 { + t.Errorf("%s: formDNSNameserversFitsLimits(%v) expected event, got no event.", tc.desc, tc.nameservers) + } else if !tc.expectedEvent && len(event) > 0 { + t.Errorf("%s: formDNSNameserversFitsLimits(%v) expected no event, got event: %v", tc.desc, tc.nameservers, event) + } + } +} + +func TestMergeDNSOptions(t *testing.T) { + testOptionValue := "3" + + testCases := []struct { + desc string + existingDNSConfigOptions []string + dnsConfigOptions []v1.PodDNSConfigOption + expectedOptions []string + }{ + { + desc: "Empty dnsConfigOptions", + existingDNSConfigOptions: []string{"ndots:5", "debug"}, + dnsConfigOptions: nil, + expectedOptions: []string{"ndots:5", "debug"}, + }, + { + desc: "No duplicated entries", + existingDNSConfigOptions: []string{"ndots:5", "debug"}, + dnsConfigOptions: []v1.PodDNSConfigOption{ + {Name: "single-request"}, + {Name: "attempts", Value: &testOptionValue}, + }, + expectedOptions: []string{"ndots:5", "debug", "single-request", "attempts:3"}, + }, + { + desc: "Overwrite duplicated entries", + existingDNSConfigOptions: []string{"ndots:5", "debug"}, + dnsConfigOptions: []v1.PodDNSConfigOption{ + {Name: "ndots", Value: &testOptionValue}, + {Name: "debug"}, + {Name: "single-request"}, + {Name: "attempts", Value: &testOptionValue}, + }, + expectedOptions: []string{"ndots:3", "debug", "single-request", "attempts:3"}, + }, + } + + for _, tc := range testCases { + options := mergeDNSOptions(tc.existingDNSConfigOptions, tc.dnsConfigOptions) + // Options order may be changed after conversion. + if !sets.NewString(options...).Equal(sets.NewString(tc.expectedOptions...)) { + t.Errorf("%s: mergeDNSOptions(%v, %v)=%v, want %v", tc.desc, tc.existingDNSConfigOptions, tc.dnsConfigOptions, options, tc.expectedOptions) + } + } +} + +func TestGetPodDNSType(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 set CustomPodDNS feature gate: %v", err) + } + }() + + recorder := record.NewFakeRecorder(20) + nodeRef := &v1.ObjectReference{ + Kind: "Node", + Name: string("testNode"), + UID: types.UID("testNode"), + Namespace: "", + } + testClusterDNSDomain := "TEST" + clusterNS := "203.0.113.1" + testClusterDNS := []net.IP{net.ParseIP(clusterNS)} + + configurer := NewConfigurer(recorder, nodeRef, nil, nil, testClusterDNSDomain, "") + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: "", + Name: "test_pod", + Namespace: "testNS", + Annotations: map[string]string{}, + }, + } + + testCases := []struct { + desc string + customPodDNSFeatureGate bool + hasClusterDNS bool + hostNetwork bool + dnsPolicy v1.DNSPolicy + expectedDNSType podDNSType + expectedError bool + }{ + { + desc: "valid DNSClusterFirst without hostnetwork", + hasClusterDNS: true, + dnsPolicy: v1.DNSClusterFirst, + expectedDNSType: podDNSCluster, + }, + { + desc: "valid DNSClusterFirstWithHostNet with hostnetwork", + hasClusterDNS: true, + hostNetwork: true, + dnsPolicy: v1.DNSClusterFirstWithHostNet, + expectedDNSType: podDNSCluster, + }, + { + desc: "valid DNSClusterFirstWithHostNet without hostnetwork", + hasClusterDNS: true, + dnsPolicy: v1.DNSClusterFirstWithHostNet, + expectedDNSType: podDNSCluster, + }, + { + desc: "valid DNSDefault without hostnetwork", + dnsPolicy: v1.DNSDefault, + expectedDNSType: podDNSHost, + }, + { + desc: "valid DNSDefault with hostnetwork", + hostNetwork: true, + dnsPolicy: v1.DNSDefault, + expectedDNSType: podDNSHost, + }, + { + desc: "DNSClusterFirst with hostnetwork, fallback to DNSDefault", + hasClusterDNS: true, + hostNetwork: true, + dnsPolicy: v1.DNSClusterFirst, + expectedDNSType: podDNSHost, + }, + { + desc: "valid DNSNone with feature gate", + customPodDNSFeatureGate: true, + dnsPolicy: v1.DNSNone, + expectedDNSType: podDNSNone, + }, + { + desc: "DNSNone without feature gate, should return error", + dnsPolicy: v1.DNSNone, + expectedError: true, + }, + { + desc: "invalid DNS policy, should return error", + dnsPolicy: "invalidPolicy", + expectedError: true, + }, + } + + for _, tc := range testCases { + if err := utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("CustomPodDNS=%v", tc.customPodDNSFeatureGate)); err != nil { + t.Errorf("Failed to set CustomPodDNS feature gate: %v", err) + } + + if tc.hasClusterDNS { + configurer.clusterDNS = testClusterDNS + } else { + configurer.clusterDNS = nil + } + pod.Spec.DNSPolicy = tc.dnsPolicy + pod.Spec.HostNetwork = tc.hostNetwork + + resType, err := getPodDNSType(pod) + if tc.expectedError { + if err == nil { + t.Errorf("%s: GetPodDNSType(%v) got no error, want error", tc.desc, pod) + } + continue + } + if resType != tc.expectedDNSType { + t.Errorf("%s: GetPodDNSType(%v)=%v, want %v", tc.desc, pod, resType, tc.expectedDNSType) + } + } +} + func TestGetPodDNS(t *testing.T) { recorder := record.NewFakeRecorder(20) nodeRef := &v1.ObjectReference{ @@ -247,6 +471,119 @@ func TestGetPodDNS(t *testing.T) { } } +func TestGetPodDNSCustom(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 set CustomPodDNS feature gate: %v", err) + } + }() + + recorder := record.NewFakeRecorder(20) + nodeRef := &v1.ObjectReference{ + Kind: "Node", + Name: string("testNode"), + UID: types.UID("testNode"), + Namespace: "", + } + clusterNS := "203.0.113.1" + testClusterDNSDomain := "kubernetes.io" + testClusterDNS := []net.IP{net.ParseIP(clusterNS)} + testOptionValue := "3" + + configurer := NewConfigurer(recorder, nodeRef, nil, testClusterDNS, testClusterDNSDomain, "") + + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + UID: "", + Name: "test_pod", + Namespace: "testNS", + Annotations: map[string]string{}, + }, + Spec: v1.PodSpec{ + DNSPolicy: v1.DNSClusterFirst, + }, + } + clusterFirstDNSConfig, err := configurer.GetPodDNS(pod) + if err != nil { + t.Fatalf("Preparing clusterFirstDNSConfig: GetPodDNS(%v), unexpected error: %v", pod, err) + } + + // Overwrite DNSPolicy for testing. + pod.Spec.DNSPolicy = v1.DNSNone + + testCases := []struct { + desc string + customPodDNSFeatureGate bool + dnsConfig *v1.PodDNSConfig + expectedDNSConfig *runtimeapi.DNSConfig + }{ + { + desc: "feature gate is disabled, DNSNone should fallback to DNSClusterFirst", + expectedDNSConfig: clusterFirstDNSConfig, + }, + { + desc: "feature gate is enabled, DNSNone without DNSConfig should have empty DNS settings", + customPodDNSFeatureGate: true, + expectedDNSConfig: &runtimeapi.DNSConfig{}, + }, + { + desc: "feature gate is enabled, DNSNone with DNSConfig should have a merged DNS settings", + customPodDNSFeatureGate: true, + dnsConfig: &v1.PodDNSConfig{ + Nameservers: []string{"10.0.0.10"}, + Searches: []string{"my.domain", "second.domain"}, + Options: []v1.PodDNSConfigOption{ + {Name: "ndots", Value: &testOptionValue}, + {Name: "debug"}, + }, + }, + expectedDNSConfig: &runtimeapi.DNSConfig{ + Servers: []string{"10.0.0.10"}, + Searches: []string{"my.domain", "second.domain"}, + Options: []string{"ndots:3", "debug"}, + }, + }, + } + + for _, tc := range testCases { + if err := utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("CustomPodDNS=%v", tc.customPodDNSFeatureGate)); err != nil { + t.Errorf("Failed to set CustomPodDNS feature gate: %v", err) + } + + pod.Spec.DNSConfig = tc.dnsConfig + + resDNSConfig, err := configurer.GetPodDNS(pod) + if err != nil { + t.Errorf("%s: GetPodDNS(%v), unexpected error: %v", tc.desc, pod, err) + } + if !dnsConfigsAreEqual(resDNSConfig, tc.expectedDNSConfig) { + t.Errorf("%s: GetPodDNS(%v)=%v, want %v", tc.desc, pod, resDNSConfig, tc.expectedDNSConfig) + } + } +} + +func dnsConfigsAreEqual(resConfig, expectedConfig *runtimeapi.DNSConfig) bool { + if len(resConfig.Servers) != len(expectedConfig.Servers) || + len(resConfig.Searches) != len(expectedConfig.Searches) || + len(resConfig.Options) != len(expectedConfig.Options) { + return false + } + for i, server := range resConfig.Servers { + if expectedConfig.Servers[i] != server { + return false + } + } + for i, search := range resConfig.Searches { + if expectedConfig.Searches[i] != search { + return false + } + } + // Options order may be changed after conversion. + return sets.NewString(resConfig.Options...).Equal(sets.NewString(expectedConfig.Options...)) +} + func newTestPods(count int) []*v1.Pod { pods := make([]*v1.Pod, count) for i := 0; i < count; i++ {