mirror of https://github.com/k3s-io/k3s
415 lines
14 KiB
Go
415 lines
14 KiB
Go
/*
|
|
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"
|
|
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"
|
|
|
|
"github.com/golang/glog"
|
|
)
|
|
|
|
var (
|
|
// The default dns opt strings.
|
|
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
|
|
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(strs []string) []string {
|
|
uniqueStrs := make(map[string]bool)
|
|
|
|
var ret []string
|
|
for _, str := range strs {
|
|
if !uniqueStrs[str] {
|
|
ret = append(ret, str)
|
|
uniqueStrs[str] = true
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (c *Configurer) formDNSSearchFitsLimits(composedSearch []string, pod *v1.Pod) []string {
|
|
limitsExceeded := false
|
|
|
|
if len(composedSearch) > validation.MaxDNSSearchPaths {
|
|
composedSearch = composedSearch[:validation.MaxDNSSearchPaths]
|
|
limitsExceeded = true
|
|
}
|
|
|
|
if resolvSearchLineStrLen := len(strings.Join(composedSearch, " ")); resolvSearchLineStrLen > validation.MaxDNSSearchListChars {
|
|
cutDomainsNum := 0
|
|
cutDomainsLen := 0
|
|
for i := len(composedSearch) - 1; i >= 0; i-- {
|
|
cutDomainsLen += len(composedSearch[i]) + 1
|
|
cutDomainsNum++
|
|
|
|
if (resolvSearchLineStrLen - cutDomainsLen) <= validation.MaxDNSSearchListChars {
|
|
break
|
|
}
|
|
}
|
|
|
|
composedSearch = composedSearch[:(len(composedSearch) - cutDomainsNum)]
|
|
limitsExceeded = true
|
|
}
|
|
|
|
if limitsExceeded {
|
|
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) 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) 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 == "" {
|
|
return hostSearch
|
|
}
|
|
|
|
nsSvcDomain := fmt.Sprintf("%s.svc.%s", pod.Namespace, c.ClusterDomain)
|
|
svcDomain := fmt.Sprintf("svc.%s", c.ClusterDomain)
|
|
clusterSearch := []string{nsSvcDomain, svcDomain, c.ClusterDomain}
|
|
|
|
return omitDuplicates(append(clusterSearch, hostSearch...))
|
|
}
|
|
|
|
// CheckLimitsForResolvConf checks limits in resolv.conf.
|
|
func (c *Configurer) CheckLimitsForResolvConf() {
|
|
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
|
|
}
|
|
|
|
domainCountLimit := validation.MaxDNSSearchPaths
|
|
|
|
if c.ClusterDomain != "" {
|
|
domainCountLimit -= 3
|
|
}
|
|
|
|
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, " ")) > 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
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// parseResolveConf reads a resolv.conf file from the given reader, and parses
|
|
// it into nameservers, searches and options, possibly returning an error.
|
|
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:]
|
|
}
|
|
}
|
|
|
|
return nameservers, searches, options, nil
|
|
}
|
|
|
|
func (c *Configurer) getHostDNSConfig(pod *v1.Pod) (*runtimeapi.DNSConfig, error) {
|
|
var hostDNS, hostSearch, hostOptions []string
|
|
// Get host DNS settings
|
|
if c.ResolverConfig != "" {
|
|
f, err := os.Open(c.ResolverConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
hostDNS, hostSearch, hostOptions, err = parseResolvConf(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &runtimeapi.DNSConfig{
|
|
Servers: hostDNS,
|
|
Searches: hostSearch,
|
|
Options: hostOptions,
|
|
}, nil
|
|
}
|
|
|
|
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
|
|
// 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 == "" {
|
|
switch {
|
|
case c.nodeIP == nil || c.nodeIP.To4() != nil:
|
|
dnsConfig.Servers = []string{"127.0.0.1"}
|
|
case c.nodeIP.To16() != nil:
|
|
dnsConfig.Servers = []string{"::1"}
|
|
}
|
|
dnsConfig.Searches = []string{"."}
|
|
}
|
|
}
|
|
|
|
if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) && pod.Spec.DNSConfig != nil {
|
|
dnsConfig = appendDNSConfig(dnsConfig, pod.Spec.DNSConfig)
|
|
}
|
|
return c.formDNSConfigFitsLimits(dnsConfig, pod), 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)
|
|
}
|
|
}
|