Commit 6eb7d8cf authored by Tim Hockin's avatar Tim Hockin

Merge pull request #13759 from upmc-enterprises/pod_dns

Allow pods to have dns
parents 5fbcb722 717660aa
...@@ -23,7 +23,9 @@ The following sections detail the supported record types and layout that is ...@@ -23,7 +23,9 @@ The following sections detail the supported record types and layout that is
supported. Any other layout or names or queries that happen to work are supported. Any other layout or names or queries that happen to work are
considered implementation details and are subject to change without warning. considered implementation details and are subject to change without warning.
### A records ### Services
#### A records
"Normal" (not headless) Services are assigned a DNS A record for a name of the "Normal" (not headless) Services are assigned a DNS A record for a name of the
form `my-svc.my-namespace.svc.cluster.local`. This resolves to the cluster IP form `my-svc.my-namespace.svc.cluster.local`. This resolves to the cluster IP
of the Service. of the Service.
...@@ -50,6 +52,13 @@ Previous versions of kube-dns made names of the for ...@@ -50,6 +52,13 @@ Previous versions of kube-dns made names of the for
`my-svc.my-namespace.cluster.local` (the 'svc' level was added later). This `my-svc.my-namespace.cluster.local` (the 'svc' level was added later). This
is no longer supported. is no longer supported.
### Pods
#### A Records
When enabled, pods are assigned a DNS A record in the form of `pod-ip-address.my-namespace.pod.cluster.local`.
For example, a pod with ip `1.2.3.4` in the namespace `default` with a dns name of `cluster.local` would have an entry: `1-2-3-4.default.pod.cluster.local`.
## How do I find the DNS server? ## How do I find the DNS server?
The DNS server itself runs as a Kubernetes Service. This gives it a stable IP The DNS server itself runs as a Kubernetes Service. This gives it a stable IP
address. When you run the SkyDNS service, you want to assign a static IP to use for address. When you run the SkyDNS service, you want to assign a static IP to use for
......
...@@ -17,7 +17,7 @@ description of `-domain` below. ...@@ -17,7 +17,7 @@ description of `-domain` below.
`-domain`: Set the domain under which all DNS names will be hosted. For `-domain`: Set the domain under which all DNS names will be hosted. For
example, if this is set to `kubernetes.io`, then a service named "nifty" in the example, if this is set to `kubernetes.io`, then a service named "nifty" in the
"default" namespace would be exposed through DNS as "default" namespace would be exposed through DNS as
"nifty.default.kubernetes.io". "nifty.default.svc.kubernetes.io".
`-v`: Set logging level `-v`: Set logging level
......
...@@ -61,6 +61,8 @@ const ( ...@@ -61,6 +61,8 @@ const (
resyncPeriod = 30 * time.Minute resyncPeriod = 30 * time.Minute
// A subdomain added to the user specified domain for all services. // A subdomain added to the user specified domain for all services.
serviceSubdomain = "svc" serviceSubdomain = "svc"
// A subdomain added to the user specified dmoain for all pods.
podSubdomain = "pod"
) )
type etcdClient interface { type etcdClient interface {
...@@ -222,6 +224,59 @@ func (ks *kube2sky) handleEndpointAdd(obj interface{}) { ...@@ -222,6 +224,59 @@ func (ks *kube2sky) handleEndpointAdd(obj interface{}) {
} }
} }
func (ks *kube2sky) handlePodCreate(obj interface{}) {
if e, ok := obj.(*kapi.Pod); ok {
// If the pod ip is not yet available, do not attempt to create.
if e.Status.PodIP != "" {
name := buildDNSNameString(ks.domain, podSubdomain, e.Namespace, santizeIP(e.Status.PodIP))
ks.mutateEtcdOrDie(func() error { return ks.generateRecordsForPod(name, e) })
}
}
}
func (ks *kube2sky) handlePodUpdate(old interface{}, new interface{}) {
oldPod, okOld := old.(*kapi.Pod)
newPod, okNew := new.(*kapi.Pod)
// Validate that the objects are good
if okOld && okNew {
if oldPod.Status.PodIP != newPod.Status.PodIP {
ks.handlePodDelete(oldPod)
ks.handlePodCreate(newPod)
}
} else if okNew {
ks.handlePodCreate(newPod)
} else if okOld {
ks.handlePodDelete(oldPod)
}
}
func (ks *kube2sky) handlePodDelete(obj interface{}) {
if e, ok := obj.(*kapi.Pod); ok {
if e.Status.PodIP != "" {
name := buildDNSNameString(ks.domain, podSubdomain, e.Namespace, santizeIP(e.Status.PodIP))
ks.mutateEtcdOrDie(func() error { return ks.removeDNS(name) })
}
}
}
func (ks *kube2sky) generateRecordsForPod(subdomain string, service *kapi.Pod) error {
b, err := json.Marshal(getSkyMsg(service.Status.PodIP, 0))
if err != nil {
return err
}
recordValue := string(b)
recordLabel := getHash(recordValue)
recordKey := buildDNSNameString(subdomain, recordLabel)
glog.V(2).Infof("Setting DNS record: %v -> %q, with recordKey: %v\n", subdomain, recordValue, recordKey)
if err := ks.writeSkyRecord(recordKey, recordValue); err != nil {
return err
}
return nil
}
func (ks *kube2sky) generateRecordsForPortalService(subdomain string, service *kapi.Service) error { func (ks *kube2sky) generateRecordsForPortalService(subdomain string, service *kapi.Service) error {
b, err := json.Marshal(getSkyMsg(service.Spec.ClusterIP, 0)) b, err := json.Marshal(getSkyMsg(service.Spec.ClusterIP, 0))
if err != nil { if err != nil {
...@@ -249,6 +304,10 @@ func (ks *kube2sky) generateRecordsForPortalService(subdomain string, service *k ...@@ -249,6 +304,10 @@ func (ks *kube2sky) generateRecordsForPortalService(subdomain string, service *k
return nil return nil
} }
func santizeIP(ip string) string {
return strings.Replace(ip, ".", "-", -1)
}
func buildPortSegmentString(portName string, portProtocol kapi.Protocol) string { func buildPortSegmentString(portName string, portProtocol kapi.Protocol) string {
if portName == "" { if portName == "" {
// we don't create a random name // we don't create a random name
...@@ -328,6 +387,11 @@ func createEndpointsLW(kubeClient *kclient.Client) *kcache.ListWatch { ...@@ -328,6 +387,11 @@ func createEndpointsLW(kubeClient *kclient.Client) *kcache.ListWatch {
return kcache.NewListWatchFromClient(kubeClient, "endpoints", kapi.NamespaceAll, kSelector.Everything()) return kcache.NewListWatchFromClient(kubeClient, "endpoints", kapi.NamespaceAll, kSelector.Everything())
} }
// Returns a cache.ListWatch that gets all changes to pods.
func createEndpointsPodLW(kubeClient *kclient.Client) *kcache.ListWatch {
return kcache.NewListWatchFromClient(kubeClient, "pods", kapi.NamespaceAll, kSelector.Everything())
}
func (ks *kube2sky) newService(obj interface{}) { func (ks *kube2sky) newService(obj interface{}) {
if s, ok := obj.(*kapi.Service); ok { if s, ok := obj.(*kapi.Service); ok {
name := buildDNSNameString(ks.domain, serviceSubdomain, s.Namespace, s.Name) name := buildDNSNameString(ks.domain, serviceSubdomain, s.Namespace, s.Name)
...@@ -411,6 +475,7 @@ func newKubeClient() (*kclient.Client, error) { ...@@ -411,6 +475,7 @@ func newKubeClient() (*kclient.Client, error) {
return nil, err return nil, err
} }
} }
if masterURL != "" && *argKubecfgFile == "" { if masterURL != "" && *argKubecfgFile == "" {
// Only --kube_master_url was provided. // Only --kube_master_url was provided.
config = &kclient.Config{ config = &kclient.Config{
...@@ -470,6 +535,24 @@ func watchEndpoints(kubeClient *kclient.Client, ks *kube2sky) kcache.Store { ...@@ -470,6 +535,24 @@ func watchEndpoints(kubeClient *kclient.Client, ks *kube2sky) kcache.Store {
return eStore return eStore
} }
func watchPods(kubeClient *kclient.Client, ks *kube2sky) kcache.Store {
eStore, eController := kframework.NewInformer(
createEndpointsPodLW(kubeClient),
&kapi.Pod{},
resyncPeriod,
kframework.ResourceEventHandlerFuncs{
AddFunc: ks.handlePodCreate,
UpdateFunc: func(oldObj, newObj interface{}) {
ks.handlePodUpdate(oldObj, newObj)
},
DeleteFunc: ks.handlePodDelete,
},
)
go eController.Run(util.NeverStop)
return eStore
}
func getHash(text string) string { func getHash(text string) string {
h := fnv.New32a() h := fnv.New32a()
h.Write([]byte(text)) h.Write([]byte(text))
...@@ -499,6 +582,7 @@ func main() { ...@@ -499,6 +582,7 @@ func main() {
ks.endpointsStore = watchEndpoints(kubeClient, &ks) ks.endpointsStore = watchEndpoints(kubeClient, &ks)
ks.servicesStore = watchForServices(kubeClient, &ks) ks.servicesStore = watchForServices(kubeClient, &ks)
ks.servicesStore = watchPods(kubeClient, &ks)
select {} select {}
} }
...@@ -82,6 +82,7 @@ const ( ...@@ -82,6 +82,7 @@ const (
testDomain = "cluster.local." testDomain = "cluster.local."
basePath = "/skydns/local/cluster" basePath = "/skydns/local/cluster"
serviceSubDomain = "svc" serviceSubDomain = "svc"
podSubDomain = "pod"
) )
func newKube2Sky(ec etcdClient) *kube2sky { func newKube2Sky(ec etcdClient) *kube2sky {
...@@ -94,8 +95,8 @@ func newKube2Sky(ec etcdClient) *kube2sky { ...@@ -94,8 +95,8 @@ func newKube2Sky(ec etcdClient) *kube2sky {
} }
} }
func getEtcdPathForA(name, namespace string) string { func getEtcdPathForA(name, namespace, subDomain string) string {
return path.Join(basePath, serviceSubDomain, namespace, name) return path.Join(basePath, subDomain, namespace, name)
} }
func getEtcdPathForSRV(portName, protocol, name, namespace string) string { func getEtcdPathForSRV(portName, protocol, name, namespace string) string {
...@@ -121,7 +122,7 @@ func getHostPortFromString(data string) (*hostPort, error) { ...@@ -121,7 +122,7 @@ func getHostPortFromString(data string) (*hostPort, error) {
} }
func assertDnsServiceEntryInEtcd(t *testing.T, ec *fakeEtcdClient, serviceName, namespace string, expectedHostPort *hostPort) { func assertDnsServiceEntryInEtcd(t *testing.T, ec *fakeEtcdClient, serviceName, namespace string, expectedHostPort *hostPort) {
key := getEtcdPathForA(serviceName, namespace) key := getEtcdPathForA(serviceName, namespace, serviceSubDomain)
values := ec.Get(key) values := ec.Get(key)
//require.True(t, exists) //require.True(t, exists)
require.True(t, len(values) > 0, "entry not found.") require.True(t, len(values) > 0, "entry not found.")
...@@ -130,6 +131,20 @@ func assertDnsServiceEntryInEtcd(t *testing.T, ec *fakeEtcdClient, serviceName, ...@@ -130,6 +131,20 @@ func assertDnsServiceEntryInEtcd(t *testing.T, ec *fakeEtcdClient, serviceName,
assert.Equal(t, expectedHostPort.Host, actualHostPort.Host) assert.Equal(t, expectedHostPort.Host, actualHostPort.Host)
} }
func assertDnsPodEntryInEtcd(t *testing.T, ec *fakeEtcdClient, podIP, namespace string) {
key := getEtcdPathForA(podIP, namespace, podSubDomain)
values := ec.Get(key)
//require.True(t, exists)
require.True(t, len(values) > 0, "entry not found.")
}
func assertDnsPodEntryNotInEtcd(t *testing.T, ec *fakeEtcdClient, podIP, namespace string) {
key := getEtcdPathForA(podIP, namespace, podSubDomain)
values := ec.Get(key)
//require.True(t, exists)
require.True(t, len(values) == 0, "entry found.")
}
func assertSRVEntryInEtcd(t *testing.T, ec *fakeEtcdClient, portName, protocol, serviceName, namespace string, expectedPortNumber, expectedEntriesCount int) { func assertSRVEntryInEtcd(t *testing.T, ec *fakeEtcdClient, portName, protocol, serviceName, namespace string, expectedPortNumber, expectedEntriesCount int) {
srvKey := getEtcdPathForSRV(portName, protocol, serviceName, namespace) srvKey := getEtcdPathForSRV(portName, protocol, serviceName, namespace)
values := ec.Get(srvKey) values := ec.Get(srvKey)
...@@ -173,6 +188,20 @@ func newService(namespace, serviceName, clusterIP, portName string, portNumber i ...@@ -173,6 +188,20 @@ func newService(namespace, serviceName, clusterIP, portName string, portNumber i
return service return service
} }
func newPod(namespace, podName, podIP string) kapi.Pod {
pod := kapi.Pod{
ObjectMeta: kapi.ObjectMeta{
Name: podName,
Namespace: namespace,
},
Status: kapi.PodStatus{
PodIP: podIP,
},
}
return pod
}
func newSubset() kapi.EndpointSubset { func newSubset() kapi.EndpointSubset {
subset := kapi.EndpointSubset{ subset := kapi.EndpointSubset{
Addresses: []kapi.EndpointAddress{}, Addresses: []kapi.EndpointAddress{},
...@@ -393,3 +422,46 @@ func TestBuildDNSName(t *testing.T) { ...@@ -393,3 +422,46 @@ func TestBuildDNSName(t *testing.T) {
newExpectedDNSName := "00.name.ns.svc.cluster.local." newExpectedDNSName := "00.name.ns.svc.cluster.local."
assert.Equal(t, newExpectedDNSName, buildDNSNameString(expectedDNSName, "00")) assert.Equal(t, newExpectedDNSName, buildDNSNameString(expectedDNSName, "00"))
} }
func TestPodDns(t *testing.T) {
const (
testPodIP = "1.2.3.4"
sanitizedPodIP = "1-2-3-4"
testNamespace = "default"
testPodName = "testPod"
)
ec := &fakeEtcdClient{make(map[string]string)}
k2s := newKube2Sky(ec)
// create pod without ip address yet
pod := newPod(testNamespace, testPodName, "")
k2s.handlePodCreate(&pod)
assert.Empty(t, ec.writes)
// create pod
pod = newPod(testNamespace, testPodName, testPodIP)
k2s.handlePodCreate(&pod)
assertDnsPodEntryInEtcd(t, ec, sanitizedPodIP, testNamespace)
// update pod with same ip
newPod := pod
newPod.Status.PodIP = testPodIP
k2s.handlePodUpdate(&pod, &newPod)
assertDnsPodEntryInEtcd(t, ec, sanitizedPodIP, testNamespace)
// update pod with different ip's
newPod = pod
newPod.Status.PodIP = "4.3.2.1"
k2s.handlePodUpdate(&pod, &newPod)
assertDnsPodEntryInEtcd(t, ec, "4-3-2-1", testNamespace)
assertDnsPodEntryNotInEtcd(t, ec, "1-2-3-4", testNamespace)
// Delete the pod
k2s.handlePodDelete(&newPod)
assert.Empty(t, ec.writes)
}
func TestSanitizeIP(t *testing.T) {
expectedIP := "1-2-3-4"
assert.Equal(t, expectedIP, santizeIP("1.2.3.4"))
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment