Support Custom Pod DNS in kubelet, gated by feature gate

This commit is contained in:
MrHohn 2017-11-18 19:46:24 -08:00 committed by Zihong Zheng
parent 44b5cf3e12
commit 9f9c721b20
3 changed files with 533 additions and 100 deletions

View File

@ -6,11 +6,14 @@ go_library(
importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns", importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//pkg/apis/core/validation:go_default_library",
"//pkg/features:go_default_library",
"//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library", "//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library",
"//pkg/kubelet/container:go_default_library", "//pkg/kubelet/container:go_default_library",
"//pkg/kubelet/util/format:go_default_library", "//pkg/kubelet/util/format:go_default_library",
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1: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", "//vendor/k8s.io/client-go/tools/record:go_default_library",
], ],
) )
@ -21,11 +24,14 @@ go_test(
importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns", importpath = "k8s.io/kubernetes/pkg/kubelet/network/dns",
library = ":go_default_library", library = ":go_default_library",
deps = [ deps = [
"//pkg/kubelet/apis/cri/v1alpha1/runtime:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/api/core/v1: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/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types: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", "//vendor/k8s.io/client-go/tools/record:go_default_library",
], ],
) )

View File

@ -26,7 +26,10 @@ import (
"strings" "strings"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/record" "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" runtimeapi "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/util/format" "k8s.io/kubernetes/pkg/kubelet/util/format"
@ -39,6 +42,14 @@ var (
defaultDNSOptions = []string{"ndots:5"} 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. // Configurer is used for setting up DNS resolver configuration when launching pods.
type Configurer struct { type Configurer struct {
recorder record.EventRecorder recorder record.EventRecorder
@ -67,37 +78,35 @@ func NewConfigurer(recorder record.EventRecorder, nodeRef *v1.ObjectReference, n
} }
} }
func omitDuplicates(pod *v1.Pod, combinedSearch []string) []string { func omitDuplicates(strs []string) []string {
uniqueDomains := map[string]bool{} uniqueStrs := make(map[string]bool)
for _, dnsDomain := range combinedSearch { var ret []string
if _, exists := uniqueDomains[dnsDomain]; !exists { for _, str := range strs {
combinedSearch[len(uniqueDomains)] = dnsDomain if !uniqueStrs[str] {
uniqueDomains[dnsDomain] = true ret = append(ret, str)
uniqueStrs[str] = true
} }
} }
return combinedSearch[:len(uniqueDomains)] return ret
} }
func (c *Configurer) formDNSSearchFitsLimits(pod *v1.Pod, composedSearch []string) []string { func (c *Configurer) formDNSSearchFitsLimits(composedSearch []string, pod *v1.Pod) []string {
// resolver file Search line current limitations
resolvSearchLineDNSDomainsLimit := 6
resolvSearchLineLenLimit := 255
limitsExceeded := false limitsExceeded := false
if len(composedSearch) > resolvSearchLineDNSDomainsLimit { if len(composedSearch) > validation.MaxDNSSearchPaths {
composedSearch = composedSearch[:resolvSearchLineDNSDomainsLimit] composedSearch = composedSearch[:validation.MaxDNSSearchPaths]
limitsExceeded = true limitsExceeded = true
} }
if resolvSearchhLineStrLen := len(strings.Join(composedSearch, " ")); resolvSearchhLineStrLen > resolvSearchLineLenLimit { if resolvSearchLineStrLen := len(strings.Join(composedSearch, " ")); resolvSearchLineStrLen > validation.MaxDNSSearchListChars {
cutDomainsNum := 0 cutDomainsNum := 0
cutDoaminsLen := 0 cutDomainsLen := 0
for i := len(composedSearch) - 1; i >= 0; i-- { for i := len(composedSearch) - 1; i >= 0; i-- {
cutDoaminsLen += len(composedSearch[i]) + 1 cutDomainsLen += len(composedSearch[i]) + 1
cutDomainsNum++ cutDomainsNum++
if (resolvSearchhLineStrLen - cutDoaminsLen) <= resolvSearchLineLenLimit { if (resolvSearchLineStrLen - cutDomainsLen) <= validation.MaxDNSSearchListChars {
break break
} }
} }
@ -107,39 +116,43 @@ func (c *Configurer) formDNSSearchFitsLimits(pod *v1.Pod, composedSearch []strin
} }
if limitsExceeded { 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, " ")) 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, "DNSSearchForming", log) c.recorder.Event(pod, v1.EventTypeWarning, "DNSConfigForming", log)
glog.Error(log) glog.Error(log)
} }
return composedSearch return composedSearch
} }
func (c *Configurer) formDNSSearchForDNSDefault(hostSearch []string, pod *v1.Pod) []string { func (c *Configurer) formDNSNameserversFitsLimits(nameservers []string, pod *v1.Pod) []string {
return c.formDNSSearchFitsLimits(pod, hostSearch) 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 == "" { if c.ClusterDomain == "" {
c.formDNSSearchFitsLimits(pod, hostSearch)
return hostSearch return hostSearch
} }
nsSvcDomain := fmt.Sprintf("%s.svc.%s", pod.Namespace, c.ClusterDomain) nsSvcDomain := fmt.Sprintf("%s.svc.%s", pod.Namespace, c.ClusterDomain)
svcDomain := fmt.Sprintf("svc.%s", 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...) return omitDuplicates(append(clusterSearch, hostSearch...))
combinedSearch = omitDuplicates(pod, combinedSearch)
return c.formDNSSearchFitsLimits(pod, combinedSearch)
} }
// CheckLimitsForResolvConf checks limits in resolv.conf. // CheckLimitsForResolvConf checks limits in resolv.conf.
func (c *Configurer) CheckLimitsForResolvConf() { func (c *Configurer) CheckLimitsForResolvConf() {
// resolver file Search line current limitations
resolvSearchLineDNSDomainsLimit := 6
resolvSearchLineLenLimit := 255
f, err := os.Open(c.ResolverConfig) f, err := os.Open(c.ResolverConfig)
if err != nil { if err != nil {
c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", err.Error()) c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", err.Error())
@ -155,21 +168,21 @@ func (c *Configurer) CheckLimitsForResolvConf() {
return return
} }
domainCntLimit := resolvSearchLineDNSDomainsLimit domainCountLimit := validation.MaxDNSSearchPaths
if c.ClusterDomain != "" { if c.ClusterDomain != "" {
domainCntLimit -= 3 domainCountLimit -= 3
} }
if len(hostSearch) > domainCntLimit { if len(hostSearch) > domainCountLimit {
log := fmt.Sprintf("Resolv.conf file '%s' contains search line consisting of more than %d domains!", c.ResolverConfig, domainCntLimit) 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) c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", log)
glog.Error("CheckLimitsForResolvConf: " + log) glog.Error("CheckLimitsForResolvConf: " + log)
return return
} }
if len(strings.Join(hostSearch, " ")) > 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, resolvSearchLineLenLimit) 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) c.recorder.Event(c.nodeRef, v1.EventTypeWarning, "CheckLimitsForResolvConf", log)
glog.Error("CheckLimitsForResolvConf: " + log) glog.Error("CheckLimitsForResolvConf: " + log)
return return
@ -180,7 +193,6 @@ func (c *Configurer) CheckLimitsForResolvConf() {
// parseResolveConf reads a resolv.conf file from the given reader, and parses // parseResolveConf reads a resolv.conf file from the given reader, and parses
// it into nameservers, searches and options, possibly returning an error. // 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) { func parseResolvConf(reader io.Reader) (nameservers []string, searches []string, options []string, err error) {
file, err := ioutil.ReadAll(reader) file, err := ioutil.ReadAll(reader)
if err != nil { 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 return nameservers, searches, options, nil
} }
// GetPodDNS returns DNS setttings for the pod. func (c *Configurer) getHostDNSConfig(pod *v1.Pod) (*runtimeapi.DNSConfig, error) {
func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) {
var hostDNS, hostSearch, hostOptions []string var hostDNS, hostSearch, hostOptions []string
// Get host DNS settings // Get host DNS settings
if c.ResolverConfig != "" { if c.ResolverConfig != "" {
@ -241,19 +248,117 @@ func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) {
return nil, err return nil, err
} }
} }
useClusterFirstPolicy := ((pod.Spec.DNSPolicy == v1.DNSClusterFirst && !kubecontainer.IsHostNetworkPod(pod)) || pod.Spec.DNSPolicy == v1.DNSClusterFirstWithHostNet) return &runtimeapi.DNSConfig{
if useClusterFirstPolicy && len(c.clusterDNS) == 0 { Servers: hostDNS,
// clusterDNS is not known. Searches: hostSearch,
// pod with ClusterDNSFirst Policy cannot be created Options: hostOptions,
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) }, nil
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 { 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
}
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 // When the kubelet --resolv-conf flag is set to the empty string, use
// DNS settings that override the docker default (which is to use // DNS settings that override the docker default (which is to use
// /etc/resolv.conf) and effectively disable DNS lookups. According to // /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 // local machine". A nameserver setting of localhost is equivalent to
// this documented behavior. // this documented behavior.
if c.ResolverConfig == "" { if c.ResolverConfig == "" {
hostSearch = []string{"."}
switch { switch {
case c.nodeIP == nil || c.nodeIP.To4() != nil: 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: case c.nodeIP.To16() != nil:
hostDNS = []string{"::1"} dnsConfig.Servers = []string{"::1"}
} }
} else { dnsConfig.Searches = []string{"."}
hostSearch = c.formDNSSearchForDNSDefault(hostSearch, pod)
} }
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 if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) && pod.Spec.DNSConfig != nil {
// the pod. The cluster DNS server itself will forward queries to other nameservers that is configured to use, dnsConfig = appendDNSConfig(dnsConfig, pod.Spec.DNSConfig)
// 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 c.formDNSConfigFitsLimits(dnsConfig, pod), nil
return &runtimeapi.DNSConfig{
Servers: dns,
Searches: dnsSearch,
Options: defaultDNSOptions}, nil
} }
// SetupDNSinContainerizedMounter replaces the nameserver in containerized-mounter's rootfs/etc/resolve.conf with kubelet.ClusterDNS // SetupDNSinContainerizedMounter replaces the nameserver in containerized-mounter's rootfs/etc/resolve.conf with kubelet.ClusterDNS

View File

@ -25,12 +25,26 @@ import (
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/record" "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/assert"
"github.com/stretchr/testify/require" "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) { func TestParseResolvConf(t *testing.T) {
testCases := []struct { testCases := []struct {
data string 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) recorder := record.NewFakeRecorder(20)
nodeRef := &v1.ObjectReference{ nodeRef := &v1.ObjectReference{
Kind: "Node", Kind: "Node",
@ -96,62 +110,272 @@ func TestComposeDNSSearch(t *testing.T) {
} }
testCases := []struct { testCases := []struct {
dnsNames []string
hostNames []string hostNames []string
resultSearch []string resultSearch []string
events []string events []string
}{ }{
{ {
[]string{"testNS.svc.TEST", "svc.TEST", "TEST"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST"},
[]string{},
[]string{"testNS.svc.TEST", "svc.TEST", "TEST"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST"},
[]string{}, []string{},
}, },
{ {
[]string{"testNS.svc.TEST", "svc.TEST", "TEST"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB"},
[]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{},
}, },
{ {
[]string{"testNS.svc.TEST", "svc.TEST", "TEST"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", strings.Repeat("B", 256), "BBB"},
[]string{"AAA", strings.Repeat("B", 256), "BBB"},
[]string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA"}, []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{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB", "CCC", "DDD"},
[]string{"AAA", "TEST", "BBB", "TEST", "CCC", "DDD"},
[]string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB", "CCC"}, []string{"testNS.svc.TEST", "svc.TEST", "TEST", "AAA", "BBB", "CCC"},
[]string{ []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"},
"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 { 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) assert.EqualValues(t, tc.resultSearch, dnsSearch, "test [%d]", i)
for _, expectedEvent := range tc.events { 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) event := fetchEvent(recorder)
assert.Equal(t, expected, event, "test [%d]", i) 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) { func TestGetPodDNS(t *testing.T) {
recorder := record.NewFakeRecorder(20) recorder := record.NewFakeRecorder(20)
nodeRef := &v1.ObjectReference{ 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 { func newTestPods(count int) []*v1.Pod {
pods := make([]*v1.Pod, count) pods := make([]*v1.Pod, count)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {