Publish metadata from kubernetes plugin (#2829)
* Publish metadata from kubernetes plugin * stickler fix * Add a couple tests * Add metadata section to README * Update plugin/kubernetes/README.md Co-Authored-By: Chris O'Haver <cohaver@infoblox.com> * Address nit
This commit is contained in:
parent
a1c97f82a6
commit
ffcd2f61cf
6 changed files with 218 additions and 6 deletions
|
@ -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
|
*.service.default.svc.cluster.local. 5 IN A 192.168.25.15
|
||||||
```
|
```
|
||||||
This response can be randomized using the `loadbalance` plugin
|
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
|
||||||
|
|
|
@ -495,9 +495,12 @@ func (APIConnServeTest) EpIndexReverse(string) []*object.Endpoints { return nil
|
||||||
func (APIConnServeTest) SvcIndexReverse(string) []*object.Service { return nil }
|
func (APIConnServeTest) SvcIndexReverse(string) []*object.Service { return nil }
|
||||||
func (APIConnServeTest) Modified() int64 { return time.Now().Unix() }
|
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{
|
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
|
return a
|
||||||
}
|
}
|
||||||
|
|
59
plugin/kubernetes/metadata.go
Normal file
59
plugin/kubernetes/metadata.go
Normal file
|
@ -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
|
||||||
|
}
|
126
plugin/kubernetes/metadata_test.go
Normal file
126
plugin/kubernetes/metadata_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,10 +20,15 @@ type Metadata struct {
|
||||||
// Name implements the Handler interface.
|
// Name implements the Handler interface.
|
||||||
func (m *Metadata) Name() string { return "metadata" }
|
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.
|
// ServeDNS implements the plugin.Handler interface.
|
||||||
func (m *Metadata) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
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}
|
state := request.Request{W: w, Req: r}
|
||||||
if plugin.Zones(m.Zones).Matches(state.Name()) != "" {
|
if plugin.Zones(m.Zones).Matches(state.Name()) != "" {
|
||||||
|
|
|
@ -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
|
// remote will always be 10.240.0.1 and port 40212. The local address is always 127.0.0.1 and
|
||||||
// port 53.
|
// port 53.
|
||||||
type ResponseWriter struct {
|
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).
|
// 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: ""}
|
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 {
|
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
|
port := 40212
|
||||||
if t.TCP {
|
if t.TCP {
|
||||||
return &net.TCPAddr{IP: ip, Port: port, Zone: ""}
|
return &net.TCPAddr{IP: ip, Port: port, Zone: ""}
|
||||||
|
|
Loading…
Add table
Reference in a new issue