Skip to content

Commit 60f5d39

Browse files
DNS should support hostname annotations
1 parent 198a4d8 commit 60f5d39

File tree

4 files changed

+220
-56
lines changed

4 files changed

+220
-56
lines changed

pkg/dns/serviceresolver.go

+103-47
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dns
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"hash/fnv"
67
"net"
@@ -10,8 +11,10 @@ import (
1011
"github.com/golang/glog"
1112

1213
kapi "k8s.io/kubernetes/pkg/api"
14+
kendpoints "k8s.io/kubernetes/pkg/api/endpoints"
1315
"k8s.io/kubernetes/pkg/api/errors"
1416
kclient "k8s.io/kubernetes/pkg/client/unversioned"
17+
"k8s.io/kubernetes/pkg/util/validation"
1518

1619
"github.com/skynetservices/skydns/msg"
1720
"github.com/skynetservices/skydns/server"
@@ -133,6 +136,8 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
133136
endpointPrefix := base == "endpoints"
134137
retrieveEndpoints := endpointPrefix || (len(segments) > 3 && segments[3] == "_endpoints")
135138

139+
includePorts := len(segments) > 3 && hasAllPrefixedSegments(segments[3:], "_") && segments[3] != "_endpoints"
140+
136141
// if has a portal IP and looking at svc
137142
if svc.Spec.ClusterIP != kapi.ClusterIPNone && !retrieveEndpoints {
138143
defaultService := msg.Service{
@@ -147,41 +152,41 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
147152
defaultName := buildDNSName(subdomain, defaultHash)
148153
defaultService.Key = msg.Path(defaultName)
149154

150-
if len(svc.Spec.Ports) == 0 {
155+
if len(svc.Spec.Ports) == 0 || !includePorts {
151156
return []msg.Service{defaultService}, nil
152157
}
153158

154159
services := []msg.Service{}
155-
if len(segments) == 3 {
156-
for _, p := range svc.Spec.Ports {
157-
port := p.Port
158-
if port == 0 {
159-
port = int32(p.TargetPort.IntVal)
160-
}
161-
if port == 0 {
162-
continue
163-
}
164-
if len(p.Protocol) == 0 {
165-
p.Protocol = kapi.ProtocolTCP
166-
}
167-
portName := p.Name
168-
if len(portName) == 0 {
169-
portName = fmt.Sprintf("unknown-port-%d", port)
170-
}
171-
keyName := buildDNSName(subdomain, "_"+strings.ToLower(string(p.Protocol)), "_"+portName)
172-
services = append(services,
173-
msg.Service{
174-
Host: svc.Spec.ClusterIP,
175-
Port: int(port),
176-
177-
Priority: 10,
178-
Weight: 10,
179-
Ttl: 30,
180-
181-
Key: msg.Path(keyName),
182-
},
183-
)
160+
protocolMatch, portMatch := segments[3], "*"
161+
if len(segments) > 4 {
162+
portMatch = segments[4]
163+
}
164+
for _, p := range svc.Spec.Ports {
165+
portSegment, protocolSegment, ok := matchesPortAndProtocol(p.Name, string(p.Protocol), portMatch, protocolMatch)
166+
if !ok {
167+
continue
168+
}
169+
170+
port := p.Port
171+
if port == 0 {
172+
port = int32(p.TargetPort.IntVal)
184173
}
174+
175+
keyName := buildDNSName(defaultName, protocolSegment, portSegment)
176+
services = append(services,
177+
msg.Service{
178+
Host: svc.Spec.ClusterIP,
179+
Port: int(port),
180+
181+
Priority: 10,
182+
Weight: 10,
183+
Ttl: 30,
184+
185+
TargetStrip: 2,
186+
187+
Key: msg.Path(keyName),
188+
},
189+
)
185190
}
186191
if len(services) == 0 {
187192
services = append(services, defaultService)
@@ -196,6 +201,14 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
196201
return nil, errNoSuchName
197202
}
198203

204+
hostnameMappings := noHostnameMappings
205+
if savedHostnames := endpoints.Annotations[kendpoints.PodHostnamesAnnotation]; len(savedHostnames) > 0 {
206+
mapped := make(map[string]kendpoints.HostRecord)
207+
if err = json.Unmarshal([]byte(savedHostnames), &mapped); err == nil {
208+
hostnameMappings = mapped
209+
}
210+
}
211+
199212
services := make([]msg.Service, 0, len(endpoints.Subsets)*4)
200213
for _, s := range endpoints.Subsets {
201214
for _, a := range s.Addresses {
@@ -207,38 +220,43 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
207220
Weight: 10,
208221
Ttl: 30,
209222
}
210-
defaultHash := getHash(defaultService.Host)
211-
defaultName := buildDNSName(subdomain, defaultHash)
223+
var endpointName string
224+
if hostname, ok := getHostname(&a, hostnameMappings); ok {
225+
endpointName = hostname
226+
} else {
227+
endpointName = getHash(defaultService.Host)
228+
}
229+
defaultName := buildDNSName(subdomain, endpointName)
212230
defaultService.Key = msg.Path(defaultName)
213231

232+
if !includePorts {
233+
services = append(services, defaultService)
234+
continue
235+
}
236+
237+
protocolMatch, portMatch := segments[3], "*"
238+
if len(segments) > 4 {
239+
portMatch = segments[4]
240+
}
214241
for _, p := range s.Ports {
215-
port := p.Port
216-
if port == 0 {
242+
portSegment, protocolSegment, ok := matchesPortAndProtocol(p.Name, string(p.Protocol), portMatch, protocolMatch)
243+
if !ok || p.Port == 0 {
217244
continue
218245
}
219-
if len(p.Protocol) == 0 {
220-
p.Protocol = kapi.ProtocolTCP
221-
}
222-
portName := p.Name
223-
if len(portName) == 0 {
224-
portName = fmt.Sprintf("unknown-port-%d", port)
225-
}
226-
227-
keyName := buildDNSName(subdomain, "_"+strings.ToLower(string(p.Protocol)), "_"+portName, defaultHash)
246+
keyName := buildDNSName(defaultName, protocolSegment, portSegment)
228247
services = append(services, msg.Service{
229248
Host: a.IP,
230-
Port: int(port),
249+
Port: int(p.Port),
231250

232251
Priority: 10,
233252
Weight: 10,
234253
Ttl: 30,
235254

255+
TargetStrip: 2,
256+
236257
Key: msg.Path(keyName),
237258
})
238259
}
239-
if len(services) == 0 {
240-
services = append(services, defaultService)
241-
}
242260
}
243261
}
244262
glog.V(4).Infof("Answered %s:%t with %#v", dnsName, exact, services)
@@ -279,6 +297,21 @@ func (b *ServiceResolver) ReverseRecord(name string) (*msg.Service, error) {
279297
// arpaSuffix is the standard suffix for PTR IP reverse lookups.
280298
const arpaSuffix = ".in-addr.arpa."
281299

300+
func matchesPortAndProtocol(name, protocol, matchPortSegment, matchProtocolSegment string) (portSegment string, protocolSegment string, match bool) {
301+
if len(name) == 0 {
302+
return "", "", false
303+
}
304+
portSegment = "_" + name
305+
if portSegment != matchPortSegment && matchPortSegment != "*" {
306+
return "", "", false
307+
}
308+
protocolSegment = "_" + strings.ToLower(string(protocol))
309+
if protocolSegment != matchProtocolSegment && matchProtocolSegment != "*" {
310+
return "", "", false
311+
}
312+
return portSegment, protocolSegment, true
313+
}
314+
282315
// extractIP turns a standard PTR reverse record lookup name
283316
// into an IP address
284317
func extractIP(reverseName string) (string, bool) {
@@ -309,6 +342,17 @@ func buildDNSName(labels ...string) string {
309342
return res
310343
}
311344

345+
// getHostname returns true if the provided address has a hostname, or false otherwise.
346+
func getHostname(address *kapi.EndpointAddress, podHostnames map[string]kendpoints.HostRecord) (string, bool) {
347+
if len(address.Hostname) > 0 {
348+
return address.Hostname, true
349+
}
350+
if hostRecord, exists := podHostnames[address.IP]; exists && len(validation.IsDNS1123Label(hostRecord.HostName)) == 0 {
351+
return hostRecord.HostName, true
352+
}
353+
return "", false
354+
}
355+
312356
// return a hash for the key name
313357
func getHash(text string) string {
314358
h := fnv.New32a()
@@ -321,3 +365,15 @@ func getHash(text string) string {
321365
func convertDashIPToIP(ip string) string {
322366
return strings.Join(strings.Split(ip, "-"), ".")
323367
}
368+
369+
// hasAllPrefixedSegments returns true if all provided segments have the given prefix.
370+
func hasAllPrefixedSegments(segments []string, prefix string) bool {
371+
for _, s := range segments {
372+
if !strings.HasPrefix(s, prefix) {
373+
return false
374+
}
375+
}
376+
return true
377+
}
378+
379+
var noHostnameMappings = map[string]kendpoints.HostRecord{}

test/cmd/dns.sh

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/bin/bash
2+
3+
set -o errexit
4+
set -o nounset
5+
set -o pipefail
6+
7+
OS_ROOT=$(dirname "${BASH_SOURCE}")/../..
8+
source "${OS_ROOT}/hack/lib/init.sh"
9+
os::log::stacktrace::install
10+
trap os::test::junit::reconcile_output EXIT
11+
12+
# Cleanup cluster resources created by this test
13+
(
14+
set +e
15+
oc delete svc,endpoints --all
16+
exit 0
17+
) &>/dev/null
18+
19+
20+
os::test::junit::declare_suite_start "cmd/dns"
21+
# This test validates DNS behavior
22+
23+
ns="$(oc project -q)"
24+
dig="dig @${API_HOST} -p 8053"
25+
if [[ -z "$(which dig)" ]]; then
26+
dig="echo SKIPPED TEST: dig is not installed: "
27+
fi
28+
29+
os::cmd::expect_success 'oc create -f test/testdata/services.yaml'
30+
os::cmd::try_until_success "${dig} +short headless.${ns}.svc.cluster.local"
31+
32+
ip="$( oc get svc/clusterip --template '{{ .spec.clusterIP }}' )"
33+
34+
os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local | wc -l" "2"
35+
os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local" "10.1.2.3"
36+
os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local" "10.1.2.4"
37+
os::cmd::expect_success_and_text "${dig} +short _endpoints.headless.${ns}.svc.cluster.local | wc -l" "2"
38+
os::cmd::expect_success_and_text "${dig} +short _endpoints.headless.${ns}.svc.cluster.local" "10.1.2.3"
39+
os::cmd::expect_success_and_text "${dig} +short _endpoints.headless.${ns}.svc.cluster.local" "10.1.2.4"
40+
os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local SRV" "^10 50 0 3987d90a.headless.${ns}.svc.cluster.local"
41+
os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local SRV" "^10 50 0 test2.headless.${ns}.svc.cluster.local"
42+
os::cmd::expect_success_and_text "${dig} +short test2.headless.${ns}.svc.cluster.local SRV" "^10 50 0 test2.headless.${ns}.svc.cluster.local"
43+
os::cmd::expect_success_and_text "${dig} +short _http._tcp.headless.${ns}.svc.cluster.local SRV" "^10 50 80 3987d90a.headless.${ns}.svc.cluster.local"
44+
os::cmd::expect_success_and_text "${dig} +short _http._tcp.headless.${ns}.svc.cluster.local SRV" "^10 50 80 test2.headless.${ns}.svc.cluster.local"
45+
46+
os::cmd::expect_success_and_text "${dig} +short clusterip.${ns}.svc.cluster.local" "^${ip}$"
47+
os::cmd::expect_success_and_text "${dig} +short clusterip.${ns}.svc.cluster.local SRV" "^10 100 0 [0-9a-f]+.clusterip.${ns}.svc.cluster.local"
48+
os::cmd::expect_success_and_text "${dig} +short _http._tcp.clusterip.${ns}.svc.cluster.local SRV" "^10 100 80 [0-9a-f]+.clusterip.${ns}.svc.cluster.local"
49+
os::cmd::expect_success_and_text "${dig} +short _endpoints.clusterip.${ns}.svc.cluster.local | wc -l" "2"
50+
os::cmd::expect_success_and_text "${dig} +short _endpoints.clusterip.${ns}.svc.cluster.local" "10.1.2.3"
51+
os::cmd::expect_success_and_text "${dig} +short _endpoints.clusterip.${ns}.svc.cluster.local" "10.1.2.4"
52+
53+
echo "dns: ok"
54+
os::test::junit::declare_suite_end

test/integration/dns_test.go

+14-9
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,17 @@ func TestDNS(t *testing.T) {
193193
dnsQuestionName: "headless.default.svc.cluster.local.",
194194
srv: []*dns.SRV{
195195
{
196-
Target: headlessIPHash + "._unknown-port-2345._tcp.headless.default.svc.cluster.local.",
197-
Port: 2345,
196+
Target: headlessIPHash + ".headless.default.svc.cluster.local.",
197+
Port: 0,
198+
},
199+
},
200+
},
201+
{ // SRV record for a port
202+
dnsQuestionName: "_http._tcp.headless2.default.svc.cluster.local.",
203+
srv: []*dns.SRV{
204+
{
205+
Target: headless2IPHash + ".headless2.default.svc.cluster.local.",
206+
Port: 2346,
198207
},
199208
},
200209
},
@@ -210,17 +219,13 @@ func TestDNS(t *testing.T) {
210219
dnsQuestionName: "headless2.default.svc.cluster.local.",
211220
srv: []*dns.SRV{
212221
{
213-
Target: headless2IPHash + "._http._tcp.headless2.default.svc.cluster.local.",
214-
Port: 2346,
215-
},
216-
{
217-
Target: headless2IPHash + "._other._tcp.headless2.default.svc.cluster.local.",
218-
Port: 2345,
222+
Target: headless2IPHash + ".headless2.default.svc.cluster.local.",
223+
Port: 0,
219224
},
220225
},
221226
},
222227
{ // the SRV record resolves to the IP
223-
dnsQuestionName: "other.e1.headless2.default.svc.cluster.local.",
228+
dnsQuestionName: headless2IPHash + ".headless2.default.svc.cluster.local.",
224229
expect: []*net.IP{&headless2IP},
225230
},
226231
{

test/testdata/services.yaml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
kind: List
2+
apiVersion: v1
3+
items:
4+
- kind: Service
5+
apiVersion: v1
6+
metadata:
7+
name: clusterip
8+
spec:
9+
ports:
10+
- name: http
11+
protocol: TCP
12+
port: 80
13+
- kind: Endpoints
14+
apiVersion: v1
15+
metadata:
16+
name: clusterip
17+
annotations:
18+
"endpoints.beta.kubernetes.io/hostnames-map": '{"10.1.2.4":{"HostName": "test2"}}'
19+
subsets:
20+
- addresses:
21+
- ip: 10.1.2.3
22+
- ip: 10.1.2.4
23+
ports:
24+
- name: http
25+
protocol: TCP
26+
port: 80
27+
- kind: Service
28+
apiVersion: v1
29+
metadata:
30+
name: headless
31+
spec:
32+
clusterIP: None
33+
ports:
34+
- name: http
35+
protocol: TCP
36+
port: 80
37+
- kind: Endpoints
38+
apiVersion: v1
39+
metadata:
40+
name: headless
41+
subsets:
42+
- addresses:
43+
- ip: 10.1.2.3
44+
- ip: 10.1.2.4
45+
hostname: test2
46+
ports:
47+
- name: http
48+
protocol: TCP
49+
port: 80

0 commit comments

Comments
 (0)