From 0bc2e1f62fee4df01b775ff37a66904c2952bb95 Mon Sep 17 00:00:00 2001 From: Zihong Zheng Date: Mon, 13 Nov 2017 18:26:06 -0800 Subject: [PATCH] Move DNS related kubelet codes into its own package --- pkg/kubelet/BUILD | 1 + pkg/kubelet/kubelet.go | 26 +-- pkg/kubelet/kubelet_network.go | 264 +---------------------- pkg/kubelet/kubelet_network_test.go | 205 ------------------ pkg/kubelet/kubelet_pods.go | 2 +- pkg/kubelet/network/BUILD | 1 + pkg/kubelet/network/dns/BUILD | 44 ++++ pkg/kubelet/network/dns/OWNERS | 4 + pkg/kubelet/network/dns/dns.go | 314 ++++++++++++++++++++++++++++ pkg/kubelet/network/dns/dns_test.go | 262 +++++++++++++++++++++++ 10 files changed, 644 insertions(+), 479 deletions(-) create mode 100644 pkg/kubelet/network/dns/BUILD create mode 100644 pkg/kubelet/network/dns/OWNERS create mode 100644 pkg/kubelet/network/dns/dns.go create mode 100644 pkg/kubelet/network/dns/dns_test.go diff --git a/pkg/kubelet/BUILD b/pkg/kubelet/BUILD index 4fa0bb5073..62877761d5 100644 --- a/pkg/kubelet/BUILD +++ b/pkg/kubelet/BUILD @@ -66,6 +66,7 @@ go_library( "//pkg/kubelet/metrics:go_default_library", "//pkg/kubelet/mountpod:go_default_library", "//pkg/kubelet/network:go_default_library", + "//pkg/kubelet/network/dns:go_default_library", "//pkg/kubelet/pleg:go_default_library", "//pkg/kubelet/pod:go_default_library", "//pkg/kubelet/preemption:go_default_library", diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index da3c221b37..2cb45bf7b0 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -77,6 +77,7 @@ import ( "k8s.io/kubernetes/pkg/kubelet/lifecycle" "k8s.io/kubernetes/pkg/kubelet/metrics" "k8s.io/kubernetes/pkg/kubelet/network" + "k8s.io/kubernetes/pkg/kubelet/network/dns" "k8s.io/kubernetes/pkg/kubelet/pleg" kubepod "k8s.io/kubernetes/pkg/kubelet/pod" "k8s.io/kubernetes/pkg/kubelet/preemption" @@ -477,6 +478,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, } } httpClient := &http.Client{} + parsedNodeIP := net.ParseIP(nodeIP) klet := &Kubelet{ hostname: hostname, @@ -488,8 +490,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, sourcesReady: config.NewSourcesReady(kubeDeps.PodConfig.SeenAllSources), registerNode: kubeCfg.RegisterNode, registerSchedulable: registerSchedulable, - clusterDomain: kubeCfg.ClusterDomain, - clusterDNS: clusterDNS, + dnsConfigurer: dns.NewConfigurer(kubeDeps.Recorder, nodeRef, parsedNodeIP, clusterDNS, kubeCfg.ClusterDomain, kubeCfg.ResolverConfig), serviceLister: serviceLister, nodeInfo: nodeInfo, masterServiceNamespace: masterServiceNamespace, @@ -512,11 +513,10 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, maxPods: int(kubeCfg.MaxPods), podsPerCore: int(kubeCfg.PodsPerCore), syncLoopMonitor: atomic.Value{}, - resolverConfig: kubeCfg.ResolverConfig, daemonEndpoints: daemonEndpoints, containerManager: kubeDeps.ContainerManager, containerRuntimeName: containerRuntime, - nodeIP: net.ParseIP(nodeIP), + nodeIP: parsedNodeIP, clock: clock.RealClock{}, enableControllerAttachDetach: kubeCfg.EnableControllerAttachDetach, iptClient: utilipt.New(utilexec.New(), utildbus.New(), utilipt.ProtocolIpv4), @@ -806,7 +806,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, experimentalCheckNodeCapabilitiesBeforeMount = false // Replace the nameserver in containerized-mounter's rootfs/etc/resolve.conf with kubelet.ClusterDNS // so that service name could be resolved - klet.setupDNSinContainerizedMounter(experimentalMounterPath) + klet.dnsConfigurer.SetupDNSinContainerizedMounter(experimentalMounterPath) } // setup volumeManager @@ -948,11 +948,8 @@ type Kubelet struct { // for internal book keeping; access only from within registerWithApiserver registrationCompleted bool - // If non-empty, use this for container DNS search. - clusterDomain string - - // If non-nil, use this for container DNS server. - clusterDNS []net.IP + // dnsConfigurer is used for setting up DNS resolver configuration when launching pods. + dnsConfigurer *dns.Configurer // masterServiceNamespace is the namespace that the master service is exposed in. masterServiceNamespace string @@ -1097,11 +1094,6 @@ type Kubelet struct { // Channel for sending pods to kill. podKillingCh chan *kubecontainer.PodPair - // The configuration file used as the base to generate the container's - // DNS resolver configuration file. This can be used in conjunction with - // clusterDomain and clusterDNS. - resolverConfig string - // Information about the ports which are opened by daemons on Node running this Kubelet server. daemonEndpoints *v1.NodeDaemonEndpoints @@ -1375,8 +1367,8 @@ func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) { go wait.Until(kl.podKiller, 1*time.Second, wait.NeverStop) // Start gorouting responsible for checking limits in resolv.conf - if kl.resolverConfig != "" { - go wait.Until(func() { kl.checkLimitsForResolvConf() }, 30*time.Second, wait.NeverStop) + if kl.dnsConfigurer.ResolverConfig != "" { + go wait.Until(func() { kl.dnsConfigurer.CheckLimitsForResolvConf() }, 30*time.Second, wait.NeverStop) } // Start component sync loops. diff --git a/pkg/kubelet/kubelet_network.go b/pkg/kubelet/kubelet_network.go index 9b9b588dfb..d0fee71d67 100644 --- a/pkg/kubelet/kubelet_network.go +++ b/pkg/kubelet/kubelet_network.go @@ -18,11 +18,6 @@ package kubelet import ( "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" "github.com/golang/glog" "k8s.io/api/core/v1" @@ -31,7 +26,6 @@ import ( kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" "k8s.io/kubernetes/pkg/kubelet/network" kubetypes "k8s.io/kubernetes/pkg/kubelet/types" - "k8s.io/kubernetes/pkg/kubelet/util/format" utiliptables "k8s.io/kubernetes/pkg/util/iptables" ) @@ -164,256 +158,6 @@ func (kl *Kubelet) providerRequiresNetworkingConfiguration() bool { return supported } -func omitDuplicates(pod *v1.Pod, combinedSearch []string) []string { - uniqueDomains := map[string]bool{} - - for _, dnsDomain := range combinedSearch { - if _, exists := uniqueDomains[dnsDomain]; !exists { - combinedSearch[len(uniqueDomains)] = dnsDomain - uniqueDomains[dnsDomain] = true - } - } - return combinedSearch[:len(uniqueDomains)] -} - -func (kl *Kubelet) formDNSSearchFitsLimits(pod *v1.Pod, composedSearch []string) []string { - // resolver file Search line current limitations - resolvSearchLineDNSDomainsLimit := 6 - resolvSearchLineLenLimit := 255 - limitsExceeded := false - - if len(composedSearch) > resolvSearchLineDNSDomainsLimit { - composedSearch = composedSearch[:resolvSearchLineDNSDomainsLimit] - limitsExceeded = true - } - - if resolvSearchhLineStrLen := len(strings.Join(composedSearch, " ")); resolvSearchhLineStrLen > resolvSearchLineLenLimit { - cutDomainsNum := 0 - cutDoaminsLen := 0 - for i := len(composedSearch) - 1; i >= 0; i-- { - cutDoaminsLen += len(composedSearch[i]) + 1 - cutDomainsNum++ - - if (resolvSearchhLineStrLen - cutDoaminsLen) <= resolvSearchLineLenLimit { - break - } - } - - composedSearch = composedSearch[:(len(composedSearch) - cutDomainsNum)] - limitsExceeded = true - } - - 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, " ")) - kl.recorder.Event(pod, v1.EventTypeWarning, "DNSSearchForming", log) - glog.Error(log) - } - return composedSearch -} - -func (kl *Kubelet) formDNSSearchForDNSDefault(hostSearch []string, pod *v1.Pod) []string { - return kl.formDNSSearchFitsLimits(pod, hostSearch) -} - -func (kl *Kubelet) formDNSSearch(hostSearch []string, pod *v1.Pod) []string { - if kl.clusterDomain == "" { - kl.formDNSSearchFitsLimits(pod, hostSearch) - return hostSearch - } - - nsSvcDomain := fmt.Sprintf("%s.svc.%s", pod.Namespace, kl.clusterDomain) - svcDomain := fmt.Sprintf("svc.%s", kl.clusterDomain) - dnsSearch := []string{nsSvcDomain, svcDomain, kl.clusterDomain} - - combinedSearch := append(dnsSearch, hostSearch...) - - combinedSearch = omitDuplicates(pod, combinedSearch) - return kl.formDNSSearchFitsLimits(pod, combinedSearch) -} - -func (kl *Kubelet) checkLimitsForResolvConf() { - // resolver file Search line current limitations - resolvSearchLineDNSDomainsLimit := 6 - resolvSearchLineLenLimit := 255 - - f, err := os.Open(kl.resolverConfig) - if err != nil { - kl.recorder.Event(kl.nodeRef, v1.EventTypeWarning, "checkLimitsForResolvConf", err.Error()) - glog.Error("checkLimitsForResolvConf: " + err.Error()) - return - } - defer f.Close() - - _, hostSearch, _, err := kl.parseResolvConf(f) - if err != nil { - kl.recorder.Event(kl.nodeRef, v1.EventTypeWarning, "checkLimitsForResolvConf", err.Error()) - glog.Error("checkLimitsForResolvConf: " + err.Error()) - return - } - - domainCntLimit := resolvSearchLineDNSDomainsLimit - - if kl.clusterDomain != "" { - domainCntLimit -= 3 - } - - if len(hostSearch) > domainCntLimit { - log := fmt.Sprintf("Resolv.conf file '%s' contains search line consisting of more than %d domains!", kl.resolverConfig, domainCntLimit) - kl.recorder.Event(kl.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!", kl.resolverConfig, resolvSearchLineLenLimit) - kl.recorder.Event(kl.nodeRef, v1.EventTypeWarning, "checkLimitsForResolvConf", log) - glog.Error("checkLimitsForResolvConf: " + log) - return - } - - return -} - -// 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 (kl *Kubelet) parseResolvConf(reader io.Reader) (nameservers []string, searches []string, options []string, err error) { - file, err := ioutil.ReadAll(reader) - if err != nil { - return nil, nil, nil, err - } - - // Lines of the form "nameserver 1.2.3.4" accumulate. - nameservers = []string{} - - // Lines of the form "search example.com" overrule - last one wins. - searches = []string{} - - // Lines of the form "option ndots:5 attempts:2" overrule - last one wins. - // Each option is recorded as an element in the array. - options = []string{} - - lines := strings.Split(string(file), "\n") - for l := range lines { - trimmed := strings.TrimSpace(lines[l]) - if strings.HasPrefix(trimmed, "#") { - continue - } - fields := strings.Fields(trimmed) - if len(fields) == 0 { - continue - } - if fields[0] == "nameserver" && len(fields) >= 2 { - nameservers = append(nameservers, fields[1]) - } - if fields[0] == "search" { - searches = fields[1:] - } - if fields[0] == "options" { - options = fields[1:] - } - } - - // 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 -} - -// GetClusterDNS returns a list of the DNS servers, a list of the DNS search -// domains of the cluster, and a list of resolv.conf options. -func (kl *Kubelet) GetClusterDNS(pod *v1.Pod) ([]string, []string, []string, bool, error) { - var hostDNS, hostSearch, hostOptions []string - // Get host DNS settings - if kl.resolverConfig != "" { - f, err := os.Open(kl.resolverConfig) - if err != nil { - return nil, nil, nil, false, err - } - defer f.Close() - - hostDNS, hostSearch, hostOptions, err = kl.parseResolvConf(f) - if err != nil { - return nil, nil, nil, false, err - } - } - useClusterFirstPolicy := ((pod.Spec.DNSPolicy == v1.DNSClusterFirst && !kubecontainer.IsHostNetworkPod(pod)) || pod.Spec.DNSPolicy == v1.DNSClusterFirstWithHostNet) - if useClusterFirstPolicy && len(kl.clusterDNS) == 0 { - // clusterDNS is not known. - // pod with ClusterDNSFirst Policy cannot be created - kl.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)) - kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, "MissingClusterDNS", log) - - // fallback to DNSDefault - useClusterFirstPolicy = false - } - - if !useClusterFirstPolicy { - // 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 - // the bind documentation, the behavior of the DNS client library when - // "nameservers" are not specified is to "use the nameserver on the - // local machine". A nameserver setting of localhost is equivalent to - // this documented behavior. - if kl.resolverConfig == "" { - hostSearch = []string{"."} - switch { - case kl.nodeIP == nil || kl.nodeIP.To4() != nil: - hostDNS = []string{"127.0.0.1"} - case kl.nodeIP.To16() != nil: - hostDNS = []string{"::1"} - } - } else { - hostSearch = kl.formDNSSearchForDNSDefault(hostSearch, pod) - } - return hostDNS, hostSearch, hostOptions, useClusterFirstPolicy, 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(kl.clusterDNS)) - for i, ip := range kl.clusterDNS { - dns[i] = ip.String() - } - dnsSearch := kl.formDNSSearch(hostSearch, pod) - - return dns, dnsSearch, hostOptions, useClusterFirstPolicy, nil -} - -// Replace the nameserver in containerized-mounter's rootfs/etc/resolve.conf with kubelet.ClusterDNS -func (kl *Kubelet) setupDNSinContainerizedMounter(mounterPath string) { - resolvePath := filepath.Join(strings.TrimSuffix(mounterPath, "/mounter"), "rootfs", "etc", "resolv.conf") - dnsString := "" - for _, dns := range kl.clusterDNS { - dnsString = dnsString + fmt.Sprintf("nameserver %s\n", dns) - } - if kl.resolverConfig != "" { - f, err := os.Open(kl.resolverConfig) - defer f.Close() - if err != nil { - glog.Error("Could not open resolverConf file") - } else { - _, hostSearch, _, err := kl.parseResolvConf(f) - if err != nil { - glog.Errorf("Error for parsing the reslov.conf file: %v", err) - } else { - dnsString = dnsString + "search" - for _, search := range hostSearch { - dnsString = dnsString + fmt.Sprintf(" %s", search) - } - dnsString = dnsString + "\n" - } - } - } - if err := ioutil.WriteFile(resolvePath, []byte(dnsString), 0600); err != nil { - glog.Errorf("Could not write dns nameserver in file %s, with error %v", resolvePath, err) - } -} - // syncNetworkStatus updates the network state func (kl *Kubelet) syncNetworkStatus() { // For cri integration, network state will be updated in updateRuntimeUp, @@ -536,3 +280,11 @@ func getIPTablesMark(bit int) string { value := 1 << uint(bit) return fmt.Sprintf("%#08x/%#08x", value, value) } + +// GetClusterDNS returns a list of the DNS servers, a list of the DNS search +// domains of the cluster, and a list of resolv.conf options. +// This function is defined in kubecontainer.RuntimeHelper interface so we +// have to implement it. +func (kl *Kubelet) GetClusterDNS(pod *v1.Pod) ([]string, []string, []string, bool, error) { + return kl.dnsConfigurer.GetClusterDNS(pod) +} diff --git a/pkg/kubelet/kubelet_network_test.go b/pkg/kubelet/kubelet_network_test.go index c0b4ea0288..edc91af2b2 100644 --- a/pkg/kubelet/kubelet_network_test.go +++ b/pkg/kubelet/kubelet_network_test.go @@ -19,13 +19,9 @@ package kubelet import ( "fmt" "net" - "strings" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" ) func TestNetworkHostGetsPodNotFound(t *testing.T) { @@ -189,207 +185,6 @@ func TestNodeIPParam(t *testing.T) { } } -func TestParseResolvConf(t *testing.T) { - testCases := []struct { - data string - nameservers []string - searches []string - options []string - }{ - {"", []string{}, []string{}, []string{}}, - {" ", []string{}, []string{}, []string{}}, - {"\n", []string{}, []string{}, []string{}}, - {"\t\n\t", []string{}, []string{}, []string{}}, - {"#comment\n", []string{}, []string{}, []string{}}, - {" #comment\n", []string{}, []string{}, []string{}}, - {"#comment\n#comment", []string{}, []string{}, []string{}}, - {"#comment\nnameserver", []string{}, []string{}, []string{}}, - {"#comment\nnameserver\nsearch", []string{}, []string{}, []string{}}, - {"nameserver 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, - {" nameserver 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, - {"\tnameserver 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, - {"nameserver\t1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, - {"nameserver \t 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, - {"nameserver 1.2.3.4\nnameserver 5.6.7.8", []string{"1.2.3.4", "5.6.7.8"}, []string{}, []string{}}, - {"nameserver 1.2.3.4 #comment", []string{"1.2.3.4"}, []string{}, []string{}}, - {"search foo", []string{}, []string{"foo"}, []string{}}, - {"search foo bar", []string{}, []string{"foo", "bar"}, []string{}}, - {"search foo bar bat\n", []string{}, []string{"foo", "bar", "bat"}, []string{}}, - {"search foo\nsearch bar", []string{}, []string{"bar"}, []string{}}, - {"nameserver 1.2.3.4\nsearch foo bar", []string{"1.2.3.4"}, []string{"foo", "bar"}, []string{}}, - {"nameserver 1.2.3.4\nsearch foo\nnameserver 5.6.7.8\nsearch bar", []string{"1.2.3.4", "5.6.7.8"}, []string{"bar"}, []string{}}, - {"#comment\nnameserver 1.2.3.4\n#comment\nsearch foo\ncomment", []string{"1.2.3.4"}, []string{"foo"}, []string{}}, - {"options ndots:5 attempts:2", []string{}, []string{}, []string{"ndots:5", "attempts:2"}}, - {"options ndots:1\noptions ndots:5 attempts:3", []string{}, []string{}, []string{"ndots:5", "attempts:3"}}, - {"nameserver 1.2.3.4\nsearch foo\nnameserver 5.6.7.8\nsearch bar\noptions ndots:5 attempts:4", []string{"1.2.3.4", "5.6.7.8"}, []string{"bar"}, []string{"ndots:5", "attempts:4"}}, - } - testKubelet := newTestKubelet(t, false /* controllerAttachDetachEnabled */) - defer testKubelet.Cleanup() - kubelet := testKubelet.kubelet - for i, tc := range testCases { - ns, srch, opts, err := kubelet.parseResolvConf(strings.NewReader(tc.data)) - require.NoError(t, err) - assert.EqualValues(t, tc.nameservers, ns, "test case [%d]: name servers", i) - assert.EqualValues(t, tc.searches, srch, "test case [%d] searches", i) - assert.EqualValues(t, tc.options, opts, "test case [%d] options", i) - } -} - -func TestComposeDNSSearch(t *testing.T) { - testKubelet := newTestKubelet(t, false /* controllerAttachDetachEnabled */) - defer testKubelet.Cleanup() - kubelet := testKubelet.kubelet - - recorder := record.NewFakeRecorder(20) - kubelet.recorder = recorder - - pod := podWithUIDNameNs("", "test_pod", "testNS") - kubelet.clusterDomain = "TEST" - - 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{}, - }, - - { - []string{"testNS.svc.TEST", "svc.TEST", "TEST"}, - []string{"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{"testNS.svc.TEST", "svc.TEST", "TEST"}, - []string{"AAA", "TEST", "BBB", "TEST", "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", - }, - }, - } - - fetchEvent := func(recorder *record.FakeRecorder) string { - select { - case event := <-recorder.Events: - return event - default: - return "No more events!" - } - } - - for i, tc := range testCases { - dnsSearch := kubelet.formDNSSearch(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) - event := fetchEvent(recorder) - assert.Equal(t, expected, event, "test [%d]", i) - } - } -} - -func TestGetClusterDNS(t *testing.T) { - testKubelet := newTestKubelet(t, false /* controllerAttachDetachEnabled */) - defer testKubelet.Cleanup() - kubelet := testKubelet.kubelet - - clusterNS := "203.0.113.1" - kubelet.clusterDomain = "kubernetes.io" - kubelet.clusterDNS = []net.IP{net.ParseIP(clusterNS)} - - pods := newTestPods(4) - pods[0].Spec.DNSPolicy = v1.DNSClusterFirstWithHostNet - pods[1].Spec.DNSPolicy = v1.DNSClusterFirst - pods[2].Spec.DNSPolicy = v1.DNSClusterFirst - pods[2].Spec.HostNetwork = false - pods[3].Spec.DNSPolicy = v1.DNSDefault - - options := make([]struct { - DNS []string - DNSSearch []string - }, 4) - for i, pod := range pods { - var err error - options[i].DNS, options[i].DNSSearch, _, _, err = kubelet.GetClusterDNS(pod) - if err != nil { - t.Fatalf("failed to generate container options: %v", err) - } - } - if len(options[0].DNS) != 1 || options[0].DNS[0] != clusterNS { - t.Errorf("expected nameserver %s, got %+v", clusterNS, options[0].DNS) - } - if len(options[0].DNSSearch) == 0 || options[0].DNSSearch[0] != ".svc."+kubelet.clusterDomain { - t.Errorf("expected search %s, got %+v", ".svc."+kubelet.clusterDomain, options[0].DNSSearch) - } - if len(options[1].DNS) != 1 || options[1].DNS[0] != "127.0.0.1" { - t.Errorf("expected nameserver 127.0.0.1, got %+v", options[1].DNS) - } - if len(options[1].DNSSearch) != 1 || options[1].DNSSearch[0] != "." { - t.Errorf("expected search \".\", got %+v", options[1].DNSSearch) - } - if len(options[2].DNS) != 1 || options[2].DNS[0] != clusterNS { - t.Errorf("expected nameserver %s, got %+v", clusterNS, options[2].DNS) - } - if len(options[2].DNSSearch) == 0 || options[2].DNSSearch[0] != ".svc."+kubelet.clusterDomain { - t.Errorf("expected search %s, got %+v", ".svc."+kubelet.clusterDomain, options[2].DNSSearch) - } - if len(options[3].DNS) != 1 || options[3].DNS[0] != "127.0.0.1" { - t.Errorf("expected nameserver 127.0.0.1, got %+v", options[3].DNS) - } - if len(options[3].DNSSearch) != 1 || options[3].DNSSearch[0] != "." { - t.Errorf("expected search \".\", got %+v", options[3].DNSSearch) - } - - kubelet.resolverConfig = "/etc/resolv.conf" - for i, pod := range pods { - var err error - options[i].DNS, options[i].DNSSearch, _, _, err = kubelet.GetClusterDNS(pod) - if err != nil { - t.Fatalf("failed to generate container options: %v", err) - } - } - t.Logf("nameservers %+v", options[1].DNS) - if len(options[0].DNS) != 1 { - t.Errorf("expected cluster nameserver only, got %+v", options[0].DNS) - } else if options[0].DNS[0] != clusterNS { - t.Errorf("expected nameserver %s, got %v", clusterNS, options[0].DNS[0]) - } - expLength := len(options[1].DNSSearch) + 3 - if expLength > 6 { - expLength = 6 - } - if len(options[0].DNSSearch) != expLength { - t.Errorf("expected prepend of cluster domain, got %+v", options[0].DNSSearch) - } else if options[0].DNSSearch[0] != ".svc."+kubelet.clusterDomain { - t.Errorf("expected domain %s, got %s", ".svc."+kubelet.clusterDomain, options[0].DNSSearch) - } - if len(options[2].DNS) != 1 { - t.Errorf("expected cluster nameserver only, got %+v", options[2].DNS) - } else if options[2].DNS[0] != clusterNS { - t.Errorf("expected nameserver %s, got %v", clusterNS, options[2].DNS[0]) - } - if len(options[2].DNSSearch) != expLength { - t.Errorf("expected prepend of cluster domain, got %+v", options[2].DNSSearch) - } else if options[2].DNSSearch[0] != ".svc."+kubelet.clusterDomain { - t.Errorf("expected domain %s, got %s", ".svc."+kubelet.clusterDomain, options[0].DNSSearch) - } -} - func TestGetIPTablesMark(t *testing.T) { tests := []struct { bit int diff --git a/pkg/kubelet/kubelet_pods.go b/pkg/kubelet/kubelet_pods.go index 7a5063e44a..bf2dede98a 100644 --- a/pkg/kubelet/kubelet_pods.go +++ b/pkg/kubelet/kubelet_pods.go @@ -346,7 +346,7 @@ func truncatePodHostnameIfNeeded(podName, hostname string) (string, error) { // given that pod's spec and annotations or returns an error. func (kl *Kubelet) GeneratePodHostNameAndDomain(pod *v1.Pod) (string, string, error) { // TODO(vmarmol): Handle better. - clusterDomain := kl.clusterDomain + clusterDomain := kl.dnsConfigurer.ClusterDomain hostname := pod.Name if len(pod.Spec.Hostname) > 0 { diff --git a/pkg/kubelet/network/BUILD b/pkg/kubelet/network/BUILD index a0c4f00dfb..51b32deeb2 100644 --- a/pkg/kubelet/network/BUILD +++ b/pkg/kubelet/network/BUILD @@ -41,6 +41,7 @@ filegroup( srcs = [ ":package-srcs", "//pkg/kubelet/network/cni:all-srcs", + "//pkg/kubelet/network/dns:all-srcs", "//pkg/kubelet/network/hairpin:all-srcs", "//pkg/kubelet/network/hostport:all-srcs", "//pkg/kubelet/network/kubenet:all-srcs", diff --git a/pkg/kubelet/network/dns/BUILD b/pkg/kubelet/network/dns/BUILD new file mode 100644 index 0000000000..33185f1734 --- /dev/null +++ b/pkg/kubelet/network/dns/BUILD @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["dns.go"], + importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns", + visibility = ["//visibility:public"], + deps = [ + "//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/client-go/tools/record:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["dns_test.go"], + importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns", + library = ":go_default_library", + deps = [ + "//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/client-go/tools/record:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/kubelet/network/dns/OWNERS b/pkg/kubelet/network/dns/OWNERS new file mode 100644 index 0000000000..b5c11e8c5a --- /dev/null +++ b/pkg/kubelet/network/dns/OWNERS @@ -0,0 +1,4 @@ +approvers: +- sig-network-approvers +reviewers: +- sig-network-reviewers diff --git a/pkg/kubelet/network/dns/dns.go b/pkg/kubelet/network/dns/dns.go new file mode 100644 index 0000000000..25109404f3 --- /dev/null +++ b/pkg/kubelet/network/dns/dns.go @@ -0,0 +1,314 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dns + +import ( + "fmt" + "io" + "io/ioutil" + "net" + "os" + "path/filepath" + "strings" + + "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" + "k8s.io/kubernetes/pkg/kubelet/util/format" + + "github.com/golang/glog" +) + +// Configurer is used for setting up DNS resolver configuration when launching pods. +type Configurer struct { + recorder record.EventRecorder + nodeRef *v1.ObjectReference + nodeIP net.IP + + // If non-nil, use this for container DNS server. + clusterDNS []net.IP + // If non-empty, use this for container DNS search. + ClusterDomain string + // The path to the DNS resolver configuration file used as the base to generate + // the container's DNS resolver configuration file. This can be used in + // conjunction with clusterDomain and clusterDNS. + ResolverConfig string +} + +// NewConfigurer returns a DNS configurer for launching pods. +func NewConfigurer(recorder record.EventRecorder, nodeRef *v1.ObjectReference, nodeIP net.IP, clusterDNS []net.IP, clusterDomain, resolverConfig string) *Configurer { + return &Configurer{ + recorder: recorder, + nodeRef: nodeRef, + nodeIP: nodeIP, + clusterDNS: clusterDNS, + ClusterDomain: clusterDomain, + ResolverConfig: resolverConfig, + } +} + +func omitDuplicates(pod *v1.Pod, combinedSearch []string) []string { + uniqueDomains := map[string]bool{} + + for _, dnsDomain := range combinedSearch { + if _, exists := uniqueDomains[dnsDomain]; !exists { + combinedSearch[len(uniqueDomains)] = dnsDomain + uniqueDomains[dnsDomain] = true + } + } + return combinedSearch[:len(uniqueDomains)] +} + +func (c *Configurer) formDNSSearchFitsLimits(pod *v1.Pod, composedSearch []string) []string { + // resolver file Search line current limitations + resolvSearchLineDNSDomainsLimit := 6 + resolvSearchLineLenLimit := 255 + limitsExceeded := false + + if len(composedSearch) > resolvSearchLineDNSDomainsLimit { + composedSearch = composedSearch[:resolvSearchLineDNSDomainsLimit] + limitsExceeded = true + } + + if resolvSearchhLineStrLen := len(strings.Join(composedSearch, " ")); resolvSearchhLineStrLen > resolvSearchLineLenLimit { + cutDomainsNum := 0 + cutDoaminsLen := 0 + for i := len(composedSearch) - 1; i >= 0; i-- { + cutDoaminsLen += len(composedSearch[i]) + 1 + cutDomainsNum++ + + if (resolvSearchhLineStrLen - cutDoaminsLen) <= resolvSearchLineLenLimit { + break + } + } + + composedSearch = composedSearch[:(len(composedSearch) - cutDomainsNum)] + limitsExceeded = true + } + + 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) + glog.Error(log) + } + return composedSearch +} + +func (c *Configurer) formDNSSearchForDNSDefault(hostSearch []string, pod *v1.Pod) []string { + return c.formDNSSearchFitsLimits(pod, hostSearch) +} + +func (c *Configurer) formDNSSearch(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} + + combinedSearch := append(dnsSearch, hostSearch...) + + combinedSearch = omitDuplicates(pod, combinedSearch) + return c.formDNSSearchFitsLimits(pod, combinedSearch) +} + +// 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()) + glog.Error("CheckLimitsForResolvConf: " + err.Error()) + return + } + defer f.Close() + + _, hostSearch, _, err := parseResolvConf(f) + if err != nil { + c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", err.Error()) + glog.Error("CheckLimitsForResolvConf: " + err.Error()) + return + } + + domainCntLimit := resolvSearchLineDNSDomainsLimit + + if c.ClusterDomain != "" { + domainCntLimit -= 3 + } + + if len(hostSearch) > domainCntLimit { + log := fmt.Sprintf("Resolv.conf file '%s' contains search line consisting of more than %d domains!", c.ResolverConfig, domainCntLimit) + 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) + c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", log) + glog.Error("CheckLimitsForResolvConf: " + log) + return + } + + return +} + +// 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 { + return nil, nil, nil, err + } + + // Lines of the form "nameserver 1.2.3.4" accumulate. + nameservers = []string{} + + // Lines of the form "search example.com" overrule - last one wins. + searches = []string{} + + // Lines of the form "option ndots:5 attempts:2" overrule - last one wins. + // Each option is recorded as an element in the array. + options = []string{} + + lines := strings.Split(string(file), "\n") + for l := range lines { + trimmed := strings.TrimSpace(lines[l]) + if strings.HasPrefix(trimmed, "#") { + continue + } + fields := strings.Fields(trimmed) + if len(fields) == 0 { + continue + } + if fields[0] == "nameserver" && len(fields) >= 2 { + nameservers = append(nameservers, fields[1]) + } + if fields[0] == "search" { + searches = fields[1:] + } + if fields[0] == "options" { + options = fields[1:] + } + } + + // 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 +} + +// GetClusterDNS returns a list of the DNS servers, a list of the DNS search +// domains of the cluster, and a list of resolv.conf options. +// TODO: This should return a struct. +func (c *Configurer) GetClusterDNS(pod *v1.Pod) ([]string, []string, []string, bool, error) { + var hostDNS, hostSearch, hostOptions []string + // Get host DNS settings + if c.ResolverConfig != "" { + f, err := os.Open(c.ResolverConfig) + if err != nil { + return nil, nil, nil, false, err + } + defer f.Close() + + hostDNS, hostSearch, hostOptions, err = parseResolvConf(f) + if err != nil { + return nil, nil, nil, false, 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) + + // fallback to DNSDefault + useClusterFirstPolicy = false + } + + if !useClusterFirstPolicy { + // 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 + // the bind documentation, the behavior of the DNS client library when + // "nameservers" are not specified is to "use the nameserver on the + // 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"} + case c.nodeIP.To16() != nil: + hostDNS = []string{"::1"} + } + } else { + hostSearch = c.formDNSSearchForDNSDefault(hostSearch, pod) + } + return hostDNS, hostSearch, hostOptions, useClusterFirstPolicy, 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() + } + dnsSearch := c.formDNSSearch(hostSearch, pod) + + return dns, dnsSearch, hostOptions, useClusterFirstPolicy, nil +} + +// SetupDNSinContainerizedMounter replaces the nameserver in containerized-mounter's rootfs/etc/resolve.conf with kubelet.ClusterDNS +func (c *Configurer) SetupDNSinContainerizedMounter(mounterPath string) { + resolvePath := filepath.Join(strings.TrimSuffix(mounterPath, "/mounter"), "rootfs", "etc", "resolv.conf") + dnsString := "" + for _, dns := range c.clusterDNS { + dnsString = dnsString + fmt.Sprintf("nameserver %s\n", dns) + } + if c.ResolverConfig != "" { + f, err := os.Open(c.ResolverConfig) + defer f.Close() + if err != nil { + glog.Error("Could not open resolverConf file") + } else { + _, hostSearch, _, err := parseResolvConf(f) + if err != nil { + glog.Errorf("Error for parsing the reslov.conf file: %v", err) + } else { + dnsString = dnsString + "search" + for _, search := range hostSearch { + dnsString = dnsString + fmt.Sprintf(" %s", search) + } + dnsString = dnsString + "\n" + } + } + } + if err := ioutil.WriteFile(resolvePath, []byte(dnsString), 0600); err != nil { + glog.Errorf("Could not write dns nameserver in file %s, with error %v", resolvePath, err) + } +} diff --git a/pkg/kubelet/network/dns/dns_test.go b/pkg/kubelet/network/dns/dns_test.go new file mode 100644 index 0000000000..88b5b81b2f --- /dev/null +++ b/pkg/kubelet/network/dns/dns_test.go @@ -0,0 +1,262 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dns + +import ( + "fmt" + "net" + "strings" + "testing" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseResolvConf(t *testing.T) { + testCases := []struct { + data string + nameservers []string + searches []string + options []string + }{ + {"", []string{}, []string{}, []string{}}, + {" ", []string{}, []string{}, []string{}}, + {"\n", []string{}, []string{}, []string{}}, + {"\t\n\t", []string{}, []string{}, []string{}}, + {"#comment\n", []string{}, []string{}, []string{}}, + {" #comment\n", []string{}, []string{}, []string{}}, + {"#comment\n#comment", []string{}, []string{}, []string{}}, + {"#comment\nnameserver", []string{}, []string{}, []string{}}, + {"#comment\nnameserver\nsearch", []string{}, []string{}, []string{}}, + {"nameserver 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, + {" nameserver 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, + {"\tnameserver 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, + {"nameserver\t1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, + {"nameserver \t 1.2.3.4", []string{"1.2.3.4"}, []string{}, []string{}}, + {"nameserver 1.2.3.4\nnameserver 5.6.7.8", []string{"1.2.3.4", "5.6.7.8"}, []string{}, []string{}}, + {"nameserver 1.2.3.4 #comment", []string{"1.2.3.4"}, []string{}, []string{}}, + {"search foo", []string{}, []string{"foo"}, []string{}}, + {"search foo bar", []string{}, []string{"foo", "bar"}, []string{}}, + {"search foo bar bat\n", []string{}, []string{"foo", "bar", "bat"}, []string{}}, + {"search foo\nsearch bar", []string{}, []string{"bar"}, []string{}}, + {"nameserver 1.2.3.4\nsearch foo bar", []string{"1.2.3.4"}, []string{"foo", "bar"}, []string{}}, + {"nameserver 1.2.3.4\nsearch foo\nnameserver 5.6.7.8\nsearch bar", []string{"1.2.3.4", "5.6.7.8"}, []string{"bar"}, []string{}}, + {"#comment\nnameserver 1.2.3.4\n#comment\nsearch foo\ncomment", []string{"1.2.3.4"}, []string{"foo"}, []string{}}, + {"options ndots:5 attempts:2", []string{}, []string{}, []string{"ndots:5", "attempts:2"}}, + {"options ndots:1\noptions ndots:5 attempts:3", []string{}, []string{}, []string{"ndots:5", "attempts:3"}}, + {"nameserver 1.2.3.4\nsearch foo\nnameserver 5.6.7.8\nsearch bar\noptions ndots:5 attempts:4", []string{"1.2.3.4", "5.6.7.8"}, []string{"bar"}, []string{"ndots:5", "attempts:4"}}, + } + for i, tc := range testCases { + ns, srch, opts, err := parseResolvConf(strings.NewReader(tc.data)) + require.NoError(t, err) + assert.EqualValues(t, tc.nameservers, ns, "test case [%d]: name servers", i) + assert.EqualValues(t, tc.searches, srch, "test case [%d] searches", i) + assert.EqualValues(t, tc.options, opts, "test case [%d] options", i) + } +} + +func TestComposeDNSSearch(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 { + 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{}, + }, + + { + []string{"testNS.svc.TEST", "svc.TEST", "TEST"}, + []string{"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{"testNS.svc.TEST", "svc.TEST", "TEST"}, + []string{"AAA", "TEST", "BBB", "TEST", "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", + }, + }, + } + + 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) + assert.EqualValues(t, tc.resultSearch, dnsSearch, "test [%d]", i) + for _, expectedEvent := range tc.events { + expected := fmt.Sprintf("%s %s %s", v1.EventTypeWarning, "DNSSearchForming", expectedEvent) + event := fetchEvent(recorder) + assert.Equal(t, expected, event, "test [%d]", i) + } + } +} + +func TestGetClusterDNS(t *testing.T) { + 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)} + + configurer := NewConfigurer(recorder, nodeRef, nil, testClusterDNS, testClusterDNSDomain, "") + + pods := newTestPods(4) + pods[0].Spec.DNSPolicy = v1.DNSClusterFirstWithHostNet + pods[1].Spec.DNSPolicy = v1.DNSClusterFirst + pods[2].Spec.DNSPolicy = v1.DNSClusterFirst + pods[2].Spec.HostNetwork = false + pods[3].Spec.DNSPolicy = v1.DNSDefault + + options := make([]struct { + DNS []string + DNSSearch []string + }, 4) + for i, pod := range pods { + var err error + options[i].DNS, options[i].DNSSearch, _, _, err = configurer.GetClusterDNS(pod) + if err != nil { + t.Fatalf("failed to generate container options: %v", err) + } + } + if len(options[0].DNS) != 1 || options[0].DNS[0] != clusterNS { + t.Errorf("expected nameserver %s, got %+v", clusterNS, options[0].DNS) + } + if len(options[0].DNSSearch) == 0 || options[0].DNSSearch[0] != ".svc."+configurer.ClusterDomain { + t.Errorf("expected search %s, got %+v", ".svc."+configurer.ClusterDomain, options[0].DNSSearch) + } + if len(options[1].DNS) != 1 || options[1].DNS[0] != "127.0.0.1" { + t.Errorf("expected nameserver 127.0.0.1, got %+v", options[1].DNS) + } + if len(options[1].DNSSearch) != 1 || options[1].DNSSearch[0] != "." { + t.Errorf("expected search \".\", got %+v", options[1].DNSSearch) + } + if len(options[2].DNS) != 1 || options[2].DNS[0] != clusterNS { + t.Errorf("expected nameserver %s, got %+v", clusterNS, options[2].DNS) + } + if len(options[2].DNSSearch) == 0 || options[2].DNSSearch[0] != ".svc."+configurer.ClusterDomain { + t.Errorf("expected search %s, got %+v", ".svc."+configurer.ClusterDomain, options[2].DNSSearch) + } + if len(options[3].DNS) != 1 || options[3].DNS[0] != "127.0.0.1" { + t.Errorf("expected nameserver 127.0.0.1, got %+v", options[3].DNS) + } + if len(options[3].DNSSearch) != 1 || options[3].DNSSearch[0] != "." { + t.Errorf("expected search \".\", got %+v", options[3].DNSSearch) + } + + testResolverConfig := "/etc/resolv.conf" + configurer = NewConfigurer(recorder, nodeRef, nil, testClusterDNS, testClusterDNSDomain, testResolverConfig) + for i, pod := range pods { + var err error + options[i].DNS, options[i].DNSSearch, _, _, err = configurer.GetClusterDNS(pod) + if err != nil { + t.Fatalf("failed to generate container options: %v", err) + } + } + t.Logf("nameservers %+v", options[1].DNS) + if len(options[0].DNS) != 1 { + t.Errorf("expected cluster nameserver only, got %+v", options[0].DNS) + } else if options[0].DNS[0] != clusterNS { + t.Errorf("expected nameserver %s, got %v", clusterNS, options[0].DNS[0]) + } + expLength := len(options[1].DNSSearch) + 3 + if expLength > 6 { + expLength = 6 + } + if len(options[0].DNSSearch) != expLength { + t.Errorf("expected prepend of cluster domain, got %+v", options[0].DNSSearch) + } else if options[0].DNSSearch[0] != ".svc."+configurer.ClusterDomain { + t.Errorf("expected domain %s, got %s", ".svc."+configurer.ClusterDomain, options[0].DNSSearch) + } + if len(options[2].DNS) != 1 { + t.Errorf("expected cluster nameserver only, got %+v", options[2].DNS) + } else if options[2].DNS[0] != clusterNS { + t.Errorf("expected nameserver %s, got %v", clusterNS, options[2].DNS[0]) + } + if len(options[2].DNSSearch) != expLength { + t.Errorf("expected prepend of cluster domain, got %+v", options[2].DNSSearch) + } else if options[2].DNSSearch[0] != ".svc."+configurer.ClusterDomain { + t.Errorf("expected domain %s, got %s", ".svc."+configurer.ClusterDomain, options[0].DNSSearch) + } +} + +func newTestPods(count int) []*v1.Pod { + pods := make([]*v1.Pod, count) + for i := 0; i < count; i++ { + pods[i] = &v1.Pod{ + Spec: v1.PodSpec{ + HostNetwork: true, + }, + ObjectMeta: metav1.ObjectMeta{ + UID: types.UID(10000 + i), + Name: fmt.Sprintf("pod%d", i), + }, + } + } + return pods +}