Browse code

DNS should support hostname annotations

Clayton Coleman authored on 2016/07/22 11:02:56
Showing 5 changed files
... ...
@@ -1,6 +1,7 @@
1 1
 package dns
2 2
 
3 3
 import (
4
+	"encoding/json"
4 5
 	"fmt"
5 6
 	"hash/fnv"
6 7
 	"net"
... ...
@@ -10,8 +11,10 @@ import (
10 10
 	"github.com/golang/glog"
11 11
 
12 12
 	kapi "k8s.io/kubernetes/pkg/api"
13
+	kendpoints "k8s.io/kubernetes/pkg/api/endpoints"
13 14
 	"k8s.io/kubernetes/pkg/api/errors"
14 15
 	kclient "k8s.io/kubernetes/pkg/client/unversioned"
16
+	"k8s.io/kubernetes/pkg/util/validation"
15 17
 
16 18
 	"github.com/skynetservices/skydns/msg"
17 19
 	"github.com/skynetservices/skydns/server"
... ...
@@ -133,6 +136,8 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
133 133
 		endpointPrefix := base == "endpoints"
134 134
 		retrieveEndpoints := endpointPrefix || (len(segments) > 3 && segments[3] == "_endpoints")
135 135
 
136
+		includePorts := len(segments) > 3 && hasAllPrefixedSegments(segments[3:], "_") && segments[3] != "_endpoints"
137
+
136 138
 		// if has a portal IP and looking at svc
137 139
 		if svc.Spec.ClusterIP != kapi.ClusterIPNone && !retrieveEndpoints {
138 140
 			defaultService := msg.Service{
... ...
@@ -147,41 +152,41 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
147 147
 			defaultName := buildDNSName(subdomain, defaultHash)
148 148
 			defaultService.Key = msg.Path(defaultName)
149 149
 
150
-			if len(svc.Spec.Ports) == 0 {
150
+			if len(svc.Spec.Ports) == 0 || !includePorts {
151 151
 				return []msg.Service{defaultService}, nil
152 152
 			}
153 153
 
154 154
 			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
-					)
155
+			protocolMatch, portMatch := segments[3], "*"
156
+			if len(segments) > 4 {
157
+				portMatch = segments[4]
158
+			}
159
+			for _, p := range svc.Spec.Ports {
160
+				portSegment, protocolSegment, ok := matchesPortAndProtocol(p.Name, string(p.Protocol), portMatch, protocolMatch)
161
+				if !ok {
162
+					continue
184 163
 				}
164
+
165
+				port := p.Port
166
+				if port == 0 {
167
+					port = int32(p.TargetPort.IntVal)
168
+				}
169
+
170
+				keyName := buildDNSName(defaultName, protocolSegment, portSegment)
171
+				services = append(services,
172
+					msg.Service{
173
+						Host: svc.Spec.ClusterIP,
174
+						Port: int(port),
175
+
176
+						Priority: 10,
177
+						Weight:   10,
178
+						Ttl:      30,
179
+
180
+						TargetStrip: 2,
181
+
182
+						Key: msg.Path(keyName),
183
+					},
184
+				)
185 185
 			}
186 186
 			if len(services) == 0 {
187 187
 				services = append(services, defaultService)
... ...
@@ -196,6 +201,16 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
196 196
 			return nil, errNoSuchName
197 197
 		}
198 198
 
199
+		hostnameMappings := noHostnameMappings
200
+		if savedHostnames := endpoints.Annotations[kendpoints.PodHostnamesAnnotation]; len(savedHostnames) > 0 {
201
+			mapped := make(map[string]kendpoints.HostRecord)
202
+			if err = json.Unmarshal([]byte(savedHostnames), &mapped); err == nil {
203
+				hostnameMappings = mapped
204
+			}
205
+		}
206
+
207
+		matchHostname := len(segments) > 3 && !hasAllPrefixedSegments(segments[3:4], "_")
208
+
199 209
 		services := make([]msg.Service, 0, len(endpoints.Subsets)*4)
200 210
 		for _, s := range endpoints.Subsets {
201 211
 			for _, a := range s.Addresses {
... ...
@@ -207,38 +222,47 @@ func (b *ServiceResolver) Records(dnsName string, exact bool) ([]msg.Service, er
207 207
 					Weight:   10,
208 208
 					Ttl:      30,
209 209
 				}
210
-				defaultHash := getHash(defaultService.Host)
211
-				defaultName := buildDNSName(subdomain, defaultHash)
210
+				var endpointName string
211
+				if hostname, ok := getHostname(&a, hostnameMappings); ok {
212
+					endpointName = hostname
213
+				} else {
214
+					endpointName = getHash(defaultService.Host)
215
+				}
216
+				if matchHostname && endpointName != segments[3] {
217
+					continue
218
+				}
219
+
220
+				defaultName := buildDNSName(subdomain, endpointName)
212 221
 				defaultService.Key = msg.Path(defaultName)
213 222
 
223
+				if !includePorts {
224
+					services = append(services, defaultService)
225
+					continue
226
+				}
227
+
228
+				protocolMatch, portMatch := segments[3], "*"
229
+				if len(segments) > 4 {
230
+					portMatch = segments[4]
231
+				}
214 232
 				for _, p := range s.Ports {
215
-					port := p.Port
216
-					if port == 0 {
233
+					portSegment, protocolSegment, ok := matchesPortAndProtocol(p.Name, string(p.Protocol), portMatch, protocolMatch)
234
+					if !ok || p.Port == 0 {
217 235
 						continue
218 236
 					}
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)
237
+					keyName := buildDNSName(defaultName, protocolSegment, portSegment)
228 238
 					services = append(services, msg.Service{
229 239
 						Host: a.IP,
230
-						Port: int(port),
240
+						Port: int(p.Port),
231 241
 
232 242
 						Priority: 10,
233 243
 						Weight:   10,
234 244
 						Ttl:      30,
235 245
 
246
+						TargetStrip: 2,
247
+
236 248
 						Key: msg.Path(keyName),
237 249
 					})
238 250
 				}
239
-				if len(services) == 0 {
240
-					services = append(services, defaultService)
241
-				}
242 251
 			}
243 252
 		}
244 253
 		glog.V(4).Infof("Answered %s:%t with %#v", dnsName, exact, services)
... ...
@@ -279,6 +303,21 @@ func (b *ServiceResolver) ReverseRecord(name string) (*msg.Service, error) {
279 279
 // arpaSuffix is the standard suffix for PTR IP reverse lookups.
280 280
 const arpaSuffix = ".in-addr.arpa."
281 281
 
282
+func matchesPortAndProtocol(name, protocol, matchPortSegment, matchProtocolSegment string) (portSegment string, protocolSegment string, match bool) {
283
+	if len(name) == 0 {
284
+		return "", "", false
285
+	}
286
+	portSegment = "_" + name
287
+	if portSegment != matchPortSegment && matchPortSegment != "*" {
288
+		return "", "", false
289
+	}
290
+	protocolSegment = "_" + strings.ToLower(string(protocol))
291
+	if protocolSegment != matchProtocolSegment && matchProtocolSegment != "*" {
292
+		return "", "", false
293
+	}
294
+	return portSegment, protocolSegment, true
295
+}
296
+
282 297
 // extractIP turns a standard PTR reverse record lookup name
283 298
 // into an IP address
284 299
 func extractIP(reverseName string) (string, bool) {
... ...
@@ -309,6 +348,17 @@ func buildDNSName(labels ...string) string {
309 309
 	return res
310 310
 }
311 311
 
312
+// getHostname returns true if the provided address has a hostname, or false otherwise.
313
+func getHostname(address *kapi.EndpointAddress, podHostnames map[string]kendpoints.HostRecord) (string, bool) {
314
+	if len(address.Hostname) > 0 {
315
+		return address.Hostname, true
316
+	}
317
+	if hostRecord, exists := podHostnames[address.IP]; exists && len(validation.IsDNS1123Label(hostRecord.HostName)) == 0 {
318
+		return hostRecord.HostName, true
319
+	}
320
+	return "", false
321
+}
322
+
312 323
 // return a hash for the key name
313 324
 func getHash(text string) string {
314 325
 	h := fnv.New32a()
... ...
@@ -321,3 +371,15 @@ func getHash(text string) string {
321 321
 func convertDashIPToIP(ip string) string {
322 322
 	return strings.Join(strings.Split(ip, "-"), ".")
323 323
 }
324
+
325
+// hasAllPrefixedSegments returns true if all provided segments have the given prefix.
326
+func hasAllPrefixedSegments(segments []string, prefix string) bool {
327
+	for _, s := range segments {
328
+		if !strings.HasPrefix(s, prefix) {
329
+			return false
330
+		}
331
+	}
332
+	return true
333
+}
334
+
335
+var noHostnameMappings = map[string]kendpoints.HostRecord{}
324 336
new file mode 100755
... ...
@@ -0,0 +1,55 @@
0
+#!/bin/bash
1
+
2
+set -o errexit
3
+set -o nounset
4
+set -o pipefail
5
+
6
+OS_ROOT=$(dirname "${BASH_SOURCE}")/../..
7
+source "${OS_ROOT}/hack/lib/init.sh"
8
+os::log::stacktrace::install
9
+trap os::test::junit::reconcile_output EXIT
10
+
11
+# Cleanup cluster resources created by this test
12
+(
13
+  set +e
14
+  oc delete svc,endpoints --all
15
+  exit 0
16
+) &>/dev/null
17
+
18
+
19
+os::test::junit::declare_suite_start "cmd/dns"
20
+# This test validates DNS behavior
21
+
22
+ns="$(oc project -q)"
23
+dig="dig @${API_HOST} -p 8053"
24
+if [[ -z "$(which dig)" ]]; then
25
+  dig="echo SKIPPED TEST: dig is not installed: "
26
+fi
27
+
28
+os::cmd::expect_success 'oc create -f test/testdata/services.yaml'
29
+os::cmd::try_until_success "${dig} +short headless.${ns}.svc.cluster.local"
30
+
31
+ip="$( oc get svc/clusterip --template '{{ .spec.clusterIP }}' )"
32
+
33
+os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local | wc -l" "2"
34
+os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local" "10.1.2.3"
35
+os::cmd::expect_success_and_text "${dig} +short headless.${ns}.svc.cluster.local" "10.1.2.4"
36
+os::cmd::expect_success_and_text "${dig} +short test2.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 100 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
... ...
@@ -266,8 +266,7 @@ var _ = Describe("DNS", func() {
266 266
 				"prefix.kubernetes.default.svc",
267 267
 				"prefix.kubernetes.default.svc.cluster.local",
268 268
 
269
-				// answer wildcards on cluster service
270
-				fmt.Sprintf("prefix.headless.%s", f.Namespace.Name),
269
+				// answer wildcards on clusterIP services
271 270
 				fmt.Sprintf("prefix.clusterip.%s", f.Namespace.Name),
272 271
 			}, expect),
273 272
 
... ...
@@ -194,8 +194,17 @@ func TestDNS(t *testing.T) {
194 194
 			dnsQuestionName: "headless.default.svc.cluster.local.",
195 195
 			srv: []*dns.SRV{
196 196
 				{
197
-					Target: headlessIPHash + "._unknown-port-2345._tcp.headless.default.svc.cluster.local.",
198
-					Port:   2345,
197
+					Target: headlessIPHash + ".headless.default.svc.cluster.local.",
198
+					Port:   0,
199
+				},
200
+			},
201
+		},
202
+		{ // SRV record for a port
203
+			dnsQuestionName: "_http._tcp.headless2.default.svc.cluster.local.",
204
+			srv: []*dns.SRV{
205
+				{
206
+					Target: headless2IPHash + ".headless2.default.svc.cluster.local.",
207
+					Port:   2346,
199 208
 				},
200 209
 			},
201 210
 		},
... ...
@@ -211,17 +220,13 @@ func TestDNS(t *testing.T) {
211 211
 			dnsQuestionName: "headless2.default.svc.cluster.local.",
212 212
 			srv: []*dns.SRV{
213 213
 				{
214
-					Target: headless2IPHash + "._http._tcp.headless2.default.svc.cluster.local.",
215
-					Port:   2346,
216
-				},
217
-				{
218
-					Target: headless2IPHash + "._other._tcp.headless2.default.svc.cluster.local.",
219
-					Port:   2345,
214
+					Target: headless2IPHash + ".headless2.default.svc.cluster.local.",
215
+					Port:   0,
220 216
 				},
221 217
 			},
222 218
 		},
223 219
 		{ // the SRV record resolves to the IP
224
-			dnsQuestionName: "other.e1.headless2.default.svc.cluster.local.",
220
+			dnsQuestionName: headless2IPHash + ".headless2.default.svc.cluster.local.",
225 221
 			expect:          []*net.IP{&headless2IP},
226 222
 		},
227 223
 		{
228 224
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+kind: List
1
+apiVersion: v1
2
+items:
3
+- kind: Service
4
+  apiVersion: v1
5
+  metadata:
6
+    name: clusterip
7
+  spec:
8
+    ports:
9
+    - name: http
10
+      protocol: TCP
11
+      port: 80
12
+- kind: Endpoints
13
+  apiVersion: v1
14
+  metadata:
15
+    name: clusterip
16
+    annotations:
17
+      "endpoints.beta.kubernetes.io/hostnames-map": '{"10.1.2.4":{"HostName": "test2"}}'
18
+  subsets:
19
+  - addresses:
20
+    - ip: 10.1.2.3
21
+    - ip: 10.1.2.4
22
+    ports:
23
+    - name: http
24
+      protocol: TCP
25
+      port: 80
26
+- kind: Service
27
+  apiVersion: v1
28
+  metadata:
29
+    name: headless
30
+  spec:
31
+    clusterIP: None
32
+    ports:
33
+    - name: http
34
+      protocol: TCP
35
+      port: 80
36
+- kind: Endpoints
37
+  apiVersion: v1
38
+  metadata:
39
+    name: headless
40
+  subsets:
41
+  - addresses:
42
+    - ip: 10.1.2.3
43
+    - ip: 10.1.2.4
44
+      hostname: test2
45
+    ports:
46
+    - name: http
47
+      protocol: TCP
48
+      port: 80
0 49
\ No newline at end of file