diff --git a/plugin/kubernetes/README.md b/plugin/kubernetes/README.md index 22614db8e..68efdae0c 100644 --- a/plugin/kubernetes/README.md +++ b/plugin/kubernetes/README.md @@ -225,3 +225,17 @@ or the word "any"), then that label will match all values. The labels that acce *.service.default.svc.cluster.local. 5 IN A 192.168.25.15 ``` This response can be randomized using the `loadbalance` plugin + +## Metadata + +The kubernetes plugin will publish the following metadata, if the _metadata_ +plugin is also enabled: + + * kubernetes/endpoint: the endpoint name in the query + * kubernetes/kind: the resource kind (pod or svc) in the query + * kubernetes/namespace: the namespace in the query + * kubernetes/port-name: the port name in an SRV query + * kubernetes/protocol: the protocol in an SRV query + * kubernetes/service: the service name in the query + * kubernetes/client-namespace: the client pod's namespace, if `pods verified` mode is enabled + * kubernetes/client-pod-name: the client pod's name, if `pods verified` mode is enabled diff --git a/plugin/kubernetes/handler_test.go b/plugin/kubernetes/handler_test.go index e1a8212ca..0efd03c07 100644 --- a/plugin/kubernetes/handler_test.go +++ b/plugin/kubernetes/handler_test.go @@ -495,9 +495,12 @@ func (APIConnServeTest) EpIndexReverse(string) []*object.Endpoints { return nil func (APIConnServeTest) SvcIndexReverse(string) []*object.Service { return nil } func (APIConnServeTest) Modified() int64 { return time.Now().Unix() } -func (APIConnServeTest) PodIndex(string) []*object.Pod { +func (APIConnServeTest) PodIndex(ip string) []*object.Pod { + if ip != "10.240.0.1" { + return []*object.Pod{} + } a := []*object.Pod{ - {Namespace: "podns", PodIP: "10.240.0.1"}, // Remote IP set in test.ResponseWriter + {Namespace: "podns", Name: "foo", PodIP: "10.240.0.1"}, // Remote IP set in test.ResponseWriter } return a } diff --git a/plugin/kubernetes/metadata.go b/plugin/kubernetes/metadata.go new file mode 100644 index 000000000..323ae9e11 --- /dev/null +++ b/plugin/kubernetes/metadata.go @@ -0,0 +1,59 @@ +package kubernetes + +import ( + "context" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/request" +) + +// Metadata implements the metadata.Provider interface. +func (k *Kubernetes) Metadata(ctx context.Context, state request.Request) context.Context { + // possible optimization: cache r so it doesn't need to be calculated again in ServeDNS + r, err := parseRequest(state) + if err != nil { + metadata.SetValueFunc(ctx, "kubernetes/parse-error", func() string { + return err.Error() + }) + return ctx + } + + metadata.SetValueFunc(ctx, "kubernetes/port-name", func() string { + return r.port + }) + + metadata.SetValueFunc(ctx, "kubernetes/protocol", func() string { + return r.protocol + }) + + metadata.SetValueFunc(ctx, "kubernetes/endpoint", func() string { + return r.endpoint + }) + + metadata.SetValueFunc(ctx, "kubernetes/service", func() string { + return r.service + }) + + metadata.SetValueFunc(ctx, "kubernetes/namespace", func() string { + return r.namespace + }) + + metadata.SetValueFunc(ctx, "kubernetes/kind", func() string { + return r.podOrSvc + }) + + pod := k.podWithIP(state.IP()) + if pod == nil { + return ctx + } + + metadata.SetValueFunc(ctx, "kubernetes/client-namespace", func() string { + return pod.Namespace + }) + + metadata.SetValueFunc(ctx, "kubernetes/client-pod-name", func() string { + return pod.Name + }) + + return ctx +} diff --git a/plugin/kubernetes/metadata_test.go b/plugin/kubernetes/metadata_test.go new file mode 100644 index 000000000..44f16f13e --- /dev/null +++ b/plugin/kubernetes/metadata_test.go @@ -0,0 +1,126 @@ +package kubernetes + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/metadata" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var metadataCases = []struct { + Qname string + Qtype uint16 + RemoteIP string + Md map[string]string +}{ + { + Qname: "foo.bar.notapod.cluster.local.", Qtype: dns.TypeA, + Md: map[string]string{ + "kubernetes/parse-error": "invalid query name", + }, + }, + { + Qname: "10-240-0-1.podns.pod.cluster.local.", Qtype: dns.TypeA, + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "pod", + "kubernetes/namespace": "podns", + "kubernetes/port-name": "*", + "kubernetes/protocol": "*", + "kubernetes/service": "10-240-0-1", + "kubernetes/client-namespace": "podns", + "kubernetes/client-pod-name": "foo", + }, + }, + { + Qname: "s.ns.svc.cluster.local.", Qtype: dns.TypeA, + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "*", + "kubernetes/protocol": "*", + "kubernetes/service": "s", + "kubernetes/client-namespace": "podns", + "kubernetes/client-pod-name": "foo", + }, + }, + { + Qname: "s.ns.svc.cluster.local.", Qtype: dns.TypeA, + RemoteIP: "10.10.10.10", + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "*", + "kubernetes/protocol": "*", + "kubernetes/service": "s", + }, + }, + { + Qname: "_http._tcp.s.ns.svc.cluster.local.", Qtype: dns.TypeSRV, + RemoteIP: "10.10.10.10", + Md: map[string]string{ + "kubernetes/endpoint": "", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "http", + "kubernetes/protocol": "tcp", + "kubernetes/service": "s", + }, + }, + { + Qname: "ep.s.ns.svc.cluster.local.", Qtype: dns.TypeA, + RemoteIP: "10.10.10.10", + Md: map[string]string{ + "kubernetes/endpoint": "ep", + "kubernetes/kind": "svc", + "kubernetes/namespace": "ns", + "kubernetes/port-name": "*", + "kubernetes/protocol": "*", + "kubernetes/service": "s", + }, + }, +} + +func mapsDiffer(a, b map[string]string) bool { + if len(a) != len(b) { + return true + } + + for k, va := range a { + vb, ok := b[k] + if !ok || va != vb { + return true + } + } + return false +} + +func TestMetadata(t *testing.T) { + k := New([]string{"cluster.local."}) + k.APIConn = &APIConnServeTest{} + + for i, tc := range metadataCases { + ctx := metadata.ContextWithMetadata(context.Background()) + state := request.Request{ + Req: &dns.Msg{Question: []dns.Question{{Name: tc.Qname, Qtype: tc.Qtype}}}, + Zone: "cluster.local.", + W: &test.ResponseWriter{RemoteIP: tc.RemoteIP}, + } + + k.Metadata(ctx, state) + + md := make(map[string]string) + for _, l := range metadata.Labels(ctx) { + md[l] = metadata.ValueFunc(ctx, l)() + } + if mapsDiffer(tc.Md, md) { + t.Errorf("case %d expected metadata %v and got %v", i, tc.Md, md) + } + } +} diff --git a/plugin/metadata/metadata.go b/plugin/metadata/metadata.go index 4abe57ddf..b3f803eab 100644 --- a/plugin/metadata/metadata.go +++ b/plugin/metadata/metadata.go @@ -20,10 +20,15 @@ type Metadata struct { // Name implements the Handler interface. func (m *Metadata) Name() string { return "metadata" } +// ContextWithMetadata is exported for use by provider tests +func ContextWithMetadata(ctx context.Context) context.Context { + return context.WithValue(ctx, key{}, md{}) +} + // ServeDNS implements the plugin.Handler interface. func (m *Metadata) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - ctx = context.WithValue(ctx, key{}, md{}) + ctx = ContextWithMetadata(ctx) state := request.Request{W: w, Req: r} if plugin.Zones(m.Zones).Matches(state.Name()) != "" { diff --git a/plugin/test/responsewriter.go b/plugin/test/responsewriter.go index 77c014116..feaa8bd7e 100644 --- a/plugin/test/responsewriter.go +++ b/plugin/test/responsewriter.go @@ -10,7 +10,8 @@ import ( // remote will always be 10.240.0.1 and port 40212. The local address is always 127.0.0.1 and // port 53. type ResponseWriter struct { - TCP bool // if TCP is true we return an TCP connection instead of an UDP one. + TCP bool // if TCP is true we return an TCP connection instead of an UDP one. + RemoteIP string } // LocalAddr returns the local address, 127.0.0.1:53 (UDP, TCP if t.TCP is true). @@ -23,9 +24,13 @@ func (t *ResponseWriter) LocalAddr() net.Addr { return &net.UDPAddr{IP: ip, Port: port, Zone: ""} } -// RemoteAddr returns the remote address, always 10.240.0.1:40212 (UDP, TCP is t.TCP is true). +// RemoteAddr returns the remote address, defaults to 10.240.0.1:40212 (UDP, TCP is t.TCP is true). func (t *ResponseWriter) RemoteAddr() net.Addr { - ip := net.ParseIP("10.240.0.1") + remoteIP := "10.240.0.1" + if t.RemoteIP != "" { + remoteIP = t.RemoteIP + } + ip := net.ParseIP(remoteIP) port := 40212 if t.TCP { return &net.TCPAddr{IP: ip, Port: port, Zone: ""}