dont require/allow "_" prefix for srv wildcard fields (#472)
* dont require/allow "_" prefix for srv wildcard fields * streamline parse/validation of req name * removing nametemplate * error when zone not found, loopify unit tests
This commit is contained in:
parent
b6a2a5aeaa
commit
a6d232a622
9 changed files with 245 additions and 600 deletions
|
@ -1,15 +1,14 @@
|
||||||
# kubernetes
|
# kubernetes
|
||||||
|
|
||||||
*kubernetes* enables reading zone data from a kubernetes cluster. Record names
|
*kubernetes* enables reading zone data from a kubernetes cluster. Record names
|
||||||
are constructed as "myservice.mynamespace.coredns.local" where:
|
are constructed as "myservice.mynamespace.type.coredns.local" where:
|
||||||
|
|
||||||
* "myservice" is the name of the k8s service (this may include multiple DNS labels,
|
* "myservice" is the name of the k8s service (this may include multiple DNS labels,
|
||||||
such as "c1.myservice"),
|
such as "c1.myservice"),
|
||||||
* "mynamespace" is the k8s namespace for the service, and
|
* "mynamespace" is the k8s namespace for the service, and
|
||||||
|
* "type" is svc or pod
|
||||||
* "coredns.local" is the zone configured for `kubernetes`.
|
* "coredns.local" is the zone configured for `kubernetes`.
|
||||||
|
|
||||||
The record name format can be changed by specifying a name template in the Corefile.
|
|
||||||
|
|
||||||
## Syntax
|
## Syntax
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
|
@ -50,9 +49,6 @@ This is the default kubernetes setup, with everything specified in full:
|
||||||
# The tls cert, key and the CA cert filenames
|
# The tls cert, key and the CA cert filenames
|
||||||
tls cert key cacert
|
tls cert key cacert
|
||||||
|
|
||||||
# Assemble k8s record names with the template
|
|
||||||
template {service}.{namespace}.{type}.{zone}
|
|
||||||
|
|
||||||
# Only expose the k8s namespace "demo"
|
# Only expose the k8s namespace "demo"
|
||||||
namespaces demo
|
namespaces demo
|
||||||
|
|
||||||
|
@ -82,22 +78,12 @@ This is the default kubernetes setup, with everything specified in full:
|
||||||
|
|
||||||
Defaults:
|
Defaults:
|
||||||
* If the `namespaces` keyword is omitted, all kubernetes namespaces are exposed.
|
* If the `namespaces` keyword is omitted, all kubernetes namespaces are exposed.
|
||||||
* If the `template` keyword is omitted, the default template of "{service}.{namespace}.{type}.{zone}" is used.
|
|
||||||
* If the `resyncperiod` keyword is omitted, the default resync period is 5 minutes.
|
* If the `resyncperiod` keyword is omitted, the default resync period is 5 minutes.
|
||||||
* The `labels` keyword is only used when filtering results based on kubernetes label selector syntax
|
* The `labels` keyword is only used when filtering results based on kubernetes label selector syntax
|
||||||
is required. The label selector syntax is described in the kubernetes API documentation at:
|
is required. The label selector syntax is described in the kubernetes API documentation at:
|
||||||
http://kubernetes.io/docs/user-guide/labels/
|
http://kubernetes.io/docs/user-guide/labels/
|
||||||
* If the `pods` keyword is omitted, all pod type requests will result in NXDOMAIN
|
* If the `pods` keyword is omitted, all pod type requests will result in NXDOMAIN
|
||||||
|
|
||||||
### Template Syntax
|
|
||||||
Record name templates can be constructed using the symbolic elements:
|
|
||||||
|
|
||||||
| template symbol | description |
|
|
||||||
| `{service}` | Kubernetes object/service name. |
|
|
||||||
| `{namespace}` | The kubernetes namespace. |
|
|
||||||
| `{type}` | The type of the kubernetes object. Supports values 'svc' and 'pod'. |
|
|
||||||
| `{zone}` | The zone configured for the kubernetes middleware. |
|
|
||||||
|
|
||||||
### Basic Setup
|
### Basic Setup
|
||||||
|
|
||||||
#### Launch Kubernetes
|
#### Launch Kubernetes
|
||||||
|
@ -146,7 +132,6 @@ Build CoreDNS and launch using this configuration file:
|
||||||
kubernetes coredns.local {
|
kubernetes coredns.local {
|
||||||
resyncperiod 5m
|
resyncperiod 5m
|
||||||
endpoint http://localhost:8080
|
endpoint http://localhost:8080
|
||||||
template {service}.{namespace}.{type}.{zone}
|
|
||||||
namespaces demo
|
namespaces demo
|
||||||
# Only expose the records for kubernetes objects
|
# Only expose the records for kubernetes objects
|
||||||
# that matches this label selector.
|
# that matches this label selector.
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
|
|
||||||
"github.com/miekg/coredns/middleware"
|
"github.com/miekg/coredns/middleware"
|
||||||
"github.com/miekg/coredns/middleware/etcd/msg"
|
"github.com/miekg/coredns/middleware/etcd/msg"
|
||||||
"github.com/miekg/coredns/middleware/kubernetes/nametemplate"
|
|
||||||
"github.com/miekg/coredns/middleware/pkg/dnsutil"
|
"github.com/miekg/coredns/middleware/pkg/dnsutil"
|
||||||
dnsstrings "github.com/miekg/coredns/middleware/pkg/strings"
|
dnsstrings "github.com/miekg/coredns/middleware/pkg/strings"
|
||||||
"github.com/miekg/coredns/middleware/proxy"
|
"github.com/miekg/coredns/middleware/proxy"
|
||||||
|
@ -38,7 +37,6 @@ type Kubernetes struct {
|
||||||
APIClientKey string
|
APIClientKey string
|
||||||
APIConn *dnsController
|
APIConn *dnsController
|
||||||
ResyncPeriod time.Duration
|
ResyncPeriod time.Duration
|
||||||
NameTemplate *nametemplate.Template
|
|
||||||
Namespaces []string
|
Namespaces []string
|
||||||
LabelSelector *unversionedapi.LabelSelector
|
LabelSelector *unversionedapi.LabelSelector
|
||||||
Selector *labels.Selector
|
Selector *labels.Selector
|
||||||
|
@ -69,16 +67,22 @@ type pod struct {
|
||||||
addr string
|
addr string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type recordRequest struct {
|
||||||
|
port, protocol, endpoint, service, namespace, typeName, zone string
|
||||||
|
}
|
||||||
|
|
||||||
var errNoItems = errors.New("no items found")
|
var errNoItems = errors.New("no items found")
|
||||||
var errNsNotExposed = errors.New("namespace is not exposed")
|
var errNsNotExposed = errors.New("namespace is not exposed")
|
||||||
var errInvalidRequest = errors.New("invalid query name")
|
var errInvalidRequest = errors.New("invalid query name")
|
||||||
|
|
||||||
// Services implements the ServiceBackend interface.
|
// Services implements the ServiceBackend interface.
|
||||||
func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware.Options) ([]msg.Service, []msg.Service, error) {
|
func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware.Options) ([]msg.Service, []msg.Service, error) {
|
||||||
if state.Type() == "SRV" && !ValidSRV(state.Name()) {
|
|
||||||
return nil, nil, errInvalidRequest
|
r, e := k.parseRequest(state.Name(), state.Type())
|
||||||
|
if e != nil {
|
||||||
|
return nil, nil, e
|
||||||
}
|
}
|
||||||
s, e := k.Records(state.Name(), exact)
|
s, e := k.Records(r)
|
||||||
return s, nil, e // Haven't implemented debug queries yet.
|
return s, nil, e // Haven't implemented debug queries yet.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,85 +181,94 @@ func (k *Kubernetes) InitKubeCache() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getZoneForName returns the zone string that matches the name and a
|
func (k *Kubernetes) parseRequest(lowerCasedName, qtype string) (r recordRequest, err error) {
|
||||||
// list of the DNS labels from name that are within the zone.
|
// 3 Possible cases
|
||||||
// For example, if "coredns.local" is a zone configured for the
|
// SRV Request: _port._protocol.service.namespace.type.zone
|
||||||
// Kubernetes middleware, then getZoneForName("a.b.coredns.local")
|
// A Request (endpoint): endpoint.service.namespace.type.zone
|
||||||
// will return ("coredns.local", ["a", "b"]).
|
// A Request (service): service.namespace.type.zone
|
||||||
func (k *Kubernetes) getZoneForName(name string) (string, []string) {
|
|
||||||
var zone string
|
|
||||||
var serviceSegments []string
|
|
||||||
|
|
||||||
|
// separate zone from rest of lowerCasedName
|
||||||
|
var segs []string
|
||||||
for _, z := range k.Zones {
|
for _, z := range k.Zones {
|
||||||
if dns.IsSubDomain(z, name) {
|
if dns.IsSubDomain(z, lowerCasedName) {
|
||||||
zone = z
|
r.zone = z
|
||||||
|
|
||||||
serviceSegments = dns.SplitDomainName(name)
|
segs = dns.SplitDomainName(lowerCasedName)
|
||||||
serviceSegments = serviceSegments[:len(serviceSegments)-dns.CountLabel(zone)]
|
segs = segs[:len(segs)-dns.CountLabel(r.zone)]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if r.zone == "" {
|
||||||
return zone, serviceSegments
|
return r, errors.New("zone not found")
|
||||||
}
|
|
||||||
|
|
||||||
// stripSRVPrefix separates out the port and protocol segments, if present
|
|
||||||
// If not present, assume all ports/protocols (e.g. wildcard)
|
|
||||||
func stripSRVPrefix(name []string) (string, string, []string) {
|
|
||||||
if name[0][0] == '_' && name[1][0] == '_' {
|
|
||||||
return name[0][1:], name[1][1:], name[2:]
|
|
||||||
}
|
}
|
||||||
// no srv prefix present
|
|
||||||
return "*", "*", name
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripEndpointName(name []string) (endpoint string, nameOut []string) {
|
offset := 0
|
||||||
if len(name) == 4 {
|
if len(segs) == 5 {
|
||||||
return strings.ToLower(name[0]), name[1:]
|
// This is a SRV style request, get first two elements as port and
|
||||||
|
// protocol, stripping leading underscores if present.
|
||||||
|
if segs[0][0] == '_' {
|
||||||
|
r.port = segs[0][1:]
|
||||||
|
} else {
|
||||||
|
r.port = segs[0]
|
||||||
|
if !symbolContainsWildcard(r.port) {
|
||||||
|
return r, errors.New("srv port must start with an underscore or be a wildcard")
|
||||||
}
|
}
|
||||||
return "", name
|
}
|
||||||
|
if segs[1][0] == '_' {
|
||||||
|
r.protocol = segs[1][1:]
|
||||||
|
if r.protocol != "tcp" && r.protocol != "udp" {
|
||||||
|
return r, errors.New("invalid srv protocol: " + r.protocol)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.protocol = segs[1]
|
||||||
|
if !symbolContainsWildcard(r.protocol) {
|
||||||
|
return r, errors.New("srv protocol must start with an underscore or be a wildcard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset = 2
|
||||||
|
} else if len(segs) == 4 {
|
||||||
|
// This is an endpoint A style request. Get first element as endpoint.
|
||||||
|
r.endpoint = segs[0]
|
||||||
|
offset = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRV requests require a port and protocol
|
||||||
|
if qtype == "SRV" {
|
||||||
|
if r.port == "" || r.protocol == "" {
|
||||||
|
return r, errors.New("invalid srv request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A requests cannot have port/protocol
|
||||||
|
if qtype == "A" {
|
||||||
|
if r.port != "" && r.protocol != "" {
|
||||||
|
return r, errors.New("invalid a request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segs) == (offset + 3) {
|
||||||
|
r.service = segs[offset]
|
||||||
|
r.namespace = segs[offset+1]
|
||||||
|
r.typeName = segs[offset+2]
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, errors.New("invalid request")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records looks up services in kubernetes. If exact is true, it will lookup
|
// Records looks up services in kubernetes. If exact is true, it will lookup
|
||||||
// just this name. This is used when find matches when completing SRV lookups
|
// just this name. This is used when find matches when completing SRV lookups
|
||||||
// for instance.
|
// for instance.
|
||||||
func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) {
|
func (k *Kubernetes) Records(r recordRequest) ([]msg.Service, error) {
|
||||||
var (
|
|
||||||
serviceName string
|
|
||||||
namespace string
|
|
||||||
typeName string
|
|
||||||
)
|
|
||||||
|
|
||||||
zone, serviceSegments := k.getZoneForName(name)
|
|
||||||
port, protocol, serviceSegments := stripSRVPrefix(serviceSegments)
|
|
||||||
endpointname, serviceSegments := stripEndpointName(serviceSegments)
|
|
||||||
if len(serviceSegments) < 3 {
|
|
||||||
return nil, errNoItems
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceName = serviceSegments[0]
|
|
||||||
namespace = serviceSegments[1]
|
|
||||||
typeName = serviceSegments[2]
|
|
||||||
|
|
||||||
if namespace == "" {
|
|
||||||
err := errors.New("Parsing query string did not produce a namespace value. Assuming wildcard namespace.")
|
|
||||||
log.Printf("[WARN] %v\n", err)
|
|
||||||
namespace = "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
if serviceName == "" {
|
|
||||||
err := errors.New("Parsing query string did not produce a serviceName value. Assuming wildcard serviceName.")
|
|
||||||
log.Printf("[WARN] %v\n", err)
|
|
||||||
serviceName = "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Abort if the namespace does not contain a wildcard, and namespace is not published per CoreFile
|
// Abort if the namespace does not contain a wildcard, and namespace is not published per CoreFile
|
||||||
// Case where namespace contains a wildcard is handled in Get(...) method.
|
// Case where namespace contains a wildcard is handled in Get(...) method.
|
||||||
if (!symbolContainsWildcard(namespace)) && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(namespace, k.Namespaces)) {
|
if (!symbolContainsWildcard(r.namespace)) && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(r.namespace, k.Namespaces)) {
|
||||||
return nil, errNsNotExposed
|
return nil, errNsNotExposed
|
||||||
}
|
}
|
||||||
|
|
||||||
services, pods, err := k.Get(namespace, serviceName, endpointname, port, protocol, typeName)
|
services, pods, err := k.Get(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -264,7 +277,7 @@ func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) {
|
||||||
return nil, errNoItems
|
return nil, errNoItems
|
||||||
}
|
}
|
||||||
|
|
||||||
records := k.getRecordsForK8sItems(services, pods, zone)
|
records := k.getRecordsForK8sItems(services, pods, r.zone)
|
||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,18 +333,6 @@ func (k *Kubernetes) getRecordsForK8sItems(services []service, pods []pod, zone
|
||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves matching data from the cache.
|
|
||||||
func (k *Kubernetes) Get(namespace, servicename, endpointname, port, protocol, typeName string) (services []service, pods []pod, err error) {
|
|
||||||
switch {
|
|
||||||
case typeName == "pod":
|
|
||||||
pods, err = k.findPods(namespace, servicename)
|
|
||||||
return nil, pods, err
|
|
||||||
default:
|
|
||||||
services, err = k.findServices(namespace, servicename, endpointname, port, protocol)
|
|
||||||
return services, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ipFromPodName(podname string) string {
|
func ipFromPodName(podname string) string {
|
||||||
if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") {
|
if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") {
|
||||||
return strings.Replace(podname, "-", ".", -1)
|
return strings.Replace(podname, "-", ".", -1)
|
||||||
|
@ -362,18 +363,30 @@ func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *Kubernetes) findServices(namespace, servicename, endpointname, port, protocol string) ([]service, error) {
|
// Get retrieves matching data from the cache.
|
||||||
|
func (k *Kubernetes) Get(r recordRequest) (services []service, pods []pod, err error) {
|
||||||
|
switch {
|
||||||
|
case r.typeName == "pod":
|
||||||
|
pods, err = k.findPods(r.namespace, r.service)
|
||||||
|
return nil, pods, err
|
||||||
|
default:
|
||||||
|
services, err = k.findServices(r)
|
||||||
|
return services, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) findServices(r recordRequest) ([]service, error) {
|
||||||
serviceList := k.APIConn.ServiceList()
|
serviceList := k.APIConn.ServiceList()
|
||||||
|
|
||||||
var resultItems []service
|
var resultItems []service
|
||||||
|
|
||||||
nsWildcard := symbolContainsWildcard(namespace)
|
nsWildcard := symbolContainsWildcard(r.namespace)
|
||||||
serviceWildcard := symbolContainsWildcard(servicename)
|
serviceWildcard := symbolContainsWildcard(r.service)
|
||||||
portWildcard := symbolContainsWildcard(port)
|
portWildcard := symbolContainsWildcard(r.port) || r.port == ""
|
||||||
protocolWildcard := symbolContainsWildcard(protocol)
|
protocolWildcard := symbolContainsWildcard(r.protocol) || r.protocol == ""
|
||||||
|
|
||||||
for _, svc := range serviceList {
|
for _, svc := range serviceList {
|
||||||
if !(symbolMatches(namespace, svc.Namespace, nsWildcard) && symbolMatches(servicename, svc.Name, serviceWildcard)) {
|
if !(symbolMatches(r.namespace, svc.Namespace, nsWildcard) && symbolMatches(r.service, svc.Name, serviceWildcard)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// If namespace has a wildcard, filter results against Corefile namespace list.
|
// If namespace has a wildcard, filter results against Corefile namespace list.
|
||||||
|
@ -384,7 +397,7 @@ func (k *Kubernetes) findServices(namespace, servicename, endpointname, port, pr
|
||||||
s := service{name: svc.Name, namespace: svc.Namespace, addr: svc.Spec.ClusterIP}
|
s := service{name: svc.Name, namespace: svc.Namespace, addr: svc.Spec.ClusterIP}
|
||||||
if s.addr != api.ClusterIPNone {
|
if s.addr != api.ClusterIPNone {
|
||||||
for _, p := range svc.Spec.Ports {
|
for _, p := range svc.Spec.Ports {
|
||||||
if !(symbolMatches(port, strings.ToLower(p.Name), portWildcard) && symbolMatches(protocol, strings.ToLower(string(p.Protocol)), protocolWildcard)) {
|
if !(symbolMatches(r.port, strings.ToLower(p.Name), portWildcard) && symbolMatches(r.protocol, strings.ToLower(string(p.Protocol)), protocolWildcard)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.ports = append(s.ports, p)
|
s.ports = append(s.ports, p)
|
||||||
|
@ -405,10 +418,10 @@ func (k *Kubernetes) findServices(namespace, servicename, endpointname, port, pr
|
||||||
for _, addr := range eps.Addresses {
|
for _, addr := range eps.Addresses {
|
||||||
for _, p := range eps.Ports {
|
for _, p := range eps.Ports {
|
||||||
ephostname := endpointHostname(addr)
|
ephostname := endpointHostname(addr)
|
||||||
if endpointname != "" && endpointname != ephostname {
|
if r.endpoint != "" && r.endpoint != ephostname {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !(symbolMatches(port, strings.ToLower(p.Name), portWildcard) && symbolMatches(protocol, strings.ToLower(string(p.Protocol)), protocolWildcard)) {
|
if !(symbolMatches(r.port, strings.ToLower(p.Name), portWildcard) && symbolMatches(r.protocol, strings.ToLower(string(p.Protocol)), protocolWildcard)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.endpoints = append(s.endpoints, endpoint{addr: addr, port: p})
|
s.endpoints = append(s.endpoints, endpoint{addr: addr, port: p})
|
||||||
|
@ -422,16 +435,10 @@ func (k *Kubernetes) findServices(namespace, servicename, endpointname, port, pr
|
||||||
}
|
}
|
||||||
|
|
||||||
func symbolMatches(queryString, candidateString string, wildcard bool) bool {
|
func symbolMatches(queryString, candidateString string, wildcard bool) bool {
|
||||||
result := false
|
if wildcard {
|
||||||
switch {
|
return true
|
||||||
case !wildcard:
|
|
||||||
result = (queryString == candidateString)
|
|
||||||
case queryString == "*":
|
|
||||||
result = true
|
|
||||||
case queryString == "any":
|
|
||||||
result = true
|
|
||||||
}
|
}
|
||||||
return result
|
return queryString == candidateString
|
||||||
}
|
}
|
||||||
|
|
||||||
// getServiceRecordForIP: Gets a service record with a cluster ip matching the ip argument
|
// getServiceRecordForIP: Gets a service record with a cluster ip matching the ip argument
|
||||||
|
@ -476,57 +483,3 @@ func (k *Kubernetes) getServiceRecordForIP(ip, name string) []msg.Service {
|
||||||
func symbolContainsWildcard(symbol string) bool {
|
func symbolContainsWildcard(symbol string) bool {
|
||||||
return (strings.Contains(symbol, "*") || (symbol == "any"))
|
return (strings.Contains(symbol, "*") || (symbol == "any"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidSRV parses a server record validating _port._proto. prefix labels.
|
|
||||||
// The valid schema is:
|
|
||||||
// * Fist two segments must start with an "_",
|
|
||||||
// * Second segment must be one of _tcp|_udp|_*|_any
|
|
||||||
func ValidSRV(name string) bool {
|
|
||||||
|
|
||||||
// Does it start with a "_" ?
|
|
||||||
if len(name) > 0 && name[0] != '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// First label
|
|
||||||
first, end := dns.NextLabel(name, 0)
|
|
||||||
if end {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Second label
|
|
||||||
off, end := dns.NextLabel(name, first)
|
|
||||||
if end {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// first:off has captured _tcp. or _udp. (if present)
|
|
||||||
second := name[first:off]
|
|
||||||
if len(second) > 0 && second[0] != '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// A bit convoluted to avoid strings.ToLower
|
|
||||||
if len(second) == 5 {
|
|
||||||
// matches _tcp
|
|
||||||
if (second[1] == 't' || second[1] == 'T') && (second[2] == 'c' || second[2] == 'C') &&
|
|
||||||
(second[3] == 'p' || second[3] == 'P') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// matches _udp
|
|
||||||
if (second[1] == 'u' || second[1] == 'U') && (second[2] == 'd' || second[2] == 'D') &&
|
|
||||||
(second[3] == 'p' || second[3] == 'P') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// matches _any
|
|
||||||
if (second[1] == 'a' || second[1] == 'A') && (second[2] == 'n' || second[2] == 'N') &&
|
|
||||||
(second[3] == 'y' || second[3] == 'Y') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// matches _*
|
|
||||||
if len(second) == 3 && second[1] == '*' {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
// Test data for TestSymbolContainsWildcard cases.
|
// Test data for TestSymbolContainsWildcard cases.
|
||||||
var testdataSymbolContainsWildcard = []struct {
|
var testdataSymbolContainsWildcard = []struct {
|
||||||
|
@ -23,3 +24,114 @@ func TestSymbolContainsWildcard(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expectString(t *testing.T, function, qtype, query string, r *recordRequest, field, expected string) {
|
||||||
|
ref := reflect.ValueOf(r)
|
||||||
|
ref_f := reflect.Indirect(ref).FieldByName(field)
|
||||||
|
got := ref_f.String()
|
||||||
|
if got != expected {
|
||||||
|
t.Errorf("Expected %v(%v, \"%v\") to get %v == \"%v\". Instead got \"%v\".", function, query, qtype, field, expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRequest(t *testing.T) {
|
||||||
|
|
||||||
|
var tcs map[string]string
|
||||||
|
|
||||||
|
k := Kubernetes{Zones: []string{"inter.webs.test"}}
|
||||||
|
f := "parseRequest"
|
||||||
|
|
||||||
|
// Test a valid SRV request
|
||||||
|
//
|
||||||
|
query := "_http._tcp.webs.mynamespace.svc.inter.webs.test."
|
||||||
|
r, e := k.parseRequest(query, "SRV")
|
||||||
|
if e != nil {
|
||||||
|
t.Errorf("Expected no error from parseRequest(%v, \"SRV\"). Instead got '%v'.", query, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs = map[string]string{
|
||||||
|
"port": "http",
|
||||||
|
"protocol": "tcp",
|
||||||
|
"endpoint": "",
|
||||||
|
"service": "webs",
|
||||||
|
"namespace": "mynamespace",
|
||||||
|
"typeName": "svc",
|
||||||
|
"zone": "inter.webs.test",
|
||||||
|
}
|
||||||
|
for field, expected := range tcs {
|
||||||
|
expectString(t, f, "SRV", query, &r, field, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test wildcard acceptance
|
||||||
|
//
|
||||||
|
query = "*.any.*.any.svc.inter.webs.test."
|
||||||
|
r, e = k.parseRequest(query, "SRV")
|
||||||
|
if e != nil {
|
||||||
|
t.Errorf("Expected no error from parseRequest(\"%v\", \"SRV\"). Instead got '%v'.", query, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
tcs = map[string]string{
|
||||||
|
"port": "*",
|
||||||
|
"protocol": "any",
|
||||||
|
"endpoint": "",
|
||||||
|
"service": "*",
|
||||||
|
"namespace": "any",
|
||||||
|
"typeName": "svc",
|
||||||
|
"zone": "inter.webs.test",
|
||||||
|
}
|
||||||
|
for field, expected := range tcs {
|
||||||
|
expectString(t, f, "SRV", query, &r, field, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test A request of endpoint
|
||||||
|
//
|
||||||
|
query = "1-2-3-4.webs.mynamespace.svc.inter.webs.test."
|
||||||
|
r, e = k.parseRequest(query, "A")
|
||||||
|
if e != nil {
|
||||||
|
t.Errorf("Expected no error from parseRequest(\"%v\", \"A\"). Instead got '%v'.", query, e)
|
||||||
|
}
|
||||||
|
tcs = map[string]string{
|
||||||
|
"port": "",
|
||||||
|
"protocol": "",
|
||||||
|
"endpoint": "1-2-3-4",
|
||||||
|
"service": "webs",
|
||||||
|
"namespace": "mynamespace",
|
||||||
|
"typeName": "svc",
|
||||||
|
"zone": "inter.webs.test",
|
||||||
|
}
|
||||||
|
for field, expected := range tcs {
|
||||||
|
expectString(t, f, "A", query, &r, field, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid query tests
|
||||||
|
//
|
||||||
|
|
||||||
|
invalidAQueries := []string{
|
||||||
|
"_http._tcp.webs.mynamespace.svc.inter.webs.test.", // A requests cannot have port or protocol
|
||||||
|
"servname.ns1.srv.inter.nets.test.", // A requests must have zone that matches corefile
|
||||||
|
|
||||||
|
}
|
||||||
|
for _, q := range invalidAQueries {
|
||||||
|
_, e = k.parseRequest(q, "A")
|
||||||
|
if e == nil {
|
||||||
|
t.Errorf("Expected error from %v(\"%v\", \"A\").", f, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidSRVQueries := []string{
|
||||||
|
"webs.mynamespace.svc.inter.webs.test.", // SRV requests must have port and protocol
|
||||||
|
"_http._pcp.webs.mynamespace.svc.inter.webs.test.", // SRV protocol must be tcp or udp
|
||||||
|
"_http._tcp.ep.webs.ns.svc.inter.webs.test.", // SRV requests cannot have an endpoint
|
||||||
|
"_*._*.webs.mynamespace.svc.inter.webs.test.", // SRV request with invalid wildcards
|
||||||
|
"_http._tcp",
|
||||||
|
"_tcp.test.",
|
||||||
|
".",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, q := range invalidSRVQueries {
|
||||||
|
_, e = k.parseRequest(q, "SRV")
|
||||||
|
if e == nil {
|
||||||
|
t.Errorf("Expected error from %v(\"%v\", \"SRV\").", f, q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (k Kubernetes) records(state request.Request, exact bool) ([]msg.Service, error) {
|
func (k Kubernetes) records(state request.Request, exact bool) ([]msg.Service, error) {
|
||||||
services, err := k.Records(state.Name(), exact)
|
r, err := k.parseRequest(state.Name(), state.Type())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
services, err := k.Records(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,195 +0,0 @@
|
||||||
package nametemplate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
dns_strings "github.com/miekg/coredns/middleware/pkg/strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Likely symbols that require support:
|
|
||||||
// {id}
|
|
||||||
// {ip}
|
|
||||||
// {portname}
|
|
||||||
// {protocolname}
|
|
||||||
// {servicename}
|
|
||||||
// {namespace}
|
|
||||||
// {type} "svc" or "pod"
|
|
||||||
// {zone}
|
|
||||||
|
|
||||||
// SkyDNS normal services have an A-record of the form "{servicename}.{namespace}.{type}.{zone}"
|
|
||||||
// This resolves to the cluster IP of the service.
|
|
||||||
|
|
||||||
// SkyDNS headless services have an A-record of the form "{servicename}.{namespace}.{type}.{zone}"
|
|
||||||
// This resolves to the set of IPs of the pods selected by the Service. Clients are expected to
|
|
||||||
// consume the set or else use round-robin selection from the set.
|
|
||||||
|
|
||||||
var symbols = map[string]string{
|
|
||||||
"service": "{service}",
|
|
||||||
"namespace": "{namespace}",
|
|
||||||
"type": "{type}",
|
|
||||||
"zone": "{zone}",
|
|
||||||
}
|
|
||||||
|
|
||||||
var types = []string{
|
|
||||||
"svc",
|
|
||||||
"pod",
|
|
||||||
}
|
|
||||||
|
|
||||||
var requiredSymbols = []string{
|
|
||||||
"namespace",
|
|
||||||
"service",
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Validate that provided NameTemplate string only contains:
|
|
||||||
// * valid, known symbols, or
|
|
||||||
// * static strings
|
|
||||||
|
|
||||||
// TODO: Support collapsing multiple segments into a symbol. Either:
|
|
||||||
// * all left-over segments are used as the "service" name, or
|
|
||||||
// * some scheme like "{namespace}.{namespace}" means use
|
|
||||||
// segments concatenated with a "." for the namespace, or
|
|
||||||
// * {namespace2:4} means use segements 2->4 for the namespace.
|
|
||||||
|
|
||||||
// TODO: possibly need to store length of segmented format to handle cases
|
|
||||||
// where query string segments to a shorter or longer list than the template.
|
|
||||||
// When query string segments to shorter than template:
|
|
||||||
// * either wildcards are being used, or
|
|
||||||
// * we are not looking up an A, AAAA, or SRV record (eg NS), or
|
|
||||||
// * we can just short-circuit failure before hitting the k8s API.
|
|
||||||
// Where the query string is longer than the template, need to define which
|
|
||||||
// symbol consumes the other segments. Most likely this would be the servicename.
|
|
||||||
// Also consider how to handle static strings in the format template.
|
|
||||||
|
|
||||||
// Template holds the kubernetes template.
|
|
||||||
type Template struct {
|
|
||||||
formatString string
|
|
||||||
splitFormat []string
|
|
||||||
// Element is a map of element name :: index in the segmented record name for the named element
|
|
||||||
Element map[string]int
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTemplate use the string s the set the template.
|
|
||||||
func (t *Template) SetTemplate(s string) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
t.Element = map[string]int{}
|
|
||||||
|
|
||||||
t.formatString = s
|
|
||||||
t.splitFormat = strings.Split(t.formatString, ".")
|
|
||||||
for templateIndex, v := range t.splitFormat {
|
|
||||||
elementPositionSet := false
|
|
||||||
for name, symbol := range symbols {
|
|
||||||
if v == symbol {
|
|
||||||
t.Element[name] = templateIndex
|
|
||||||
elementPositionSet = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !elementPositionSet {
|
|
||||||
if strings.Contains(v, "{") {
|
|
||||||
err = errors.New("Record name template contains the unknown symbol '" + v + "'")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil && !t.IsValid() {
|
|
||||||
err = errors.New("Record name template does not pass NameTemplate validation")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Find a better way to pull the data segments out of the
|
|
||||||
// query string based on the template. Perhaps it is better
|
|
||||||
// to treat the query string segments as a reverse stack and
|
|
||||||
// step down the stack to find the right element.
|
|
||||||
|
|
||||||
// ZoneFromSegmentArray returns the zone string from the segments.
|
|
||||||
func (t *Template) ZoneFromSegmentArray(segments []string) string {
|
|
||||||
index, ok := t.Element["zone"]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.Join(segments[index:], ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NamespaceFromSegmentArray returns the namespace string from the segments.
|
|
||||||
func (t *Template) NamespaceFromSegmentArray(segments []string) string {
|
|
||||||
return t.symbolFromSegmentArray("namespace", segments)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceFromSegmentArray returns the service string from the segments.
|
|
||||||
func (t *Template) ServiceFromSegmentArray(segments []string) string {
|
|
||||||
return t.symbolFromSegmentArray("service", segments)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeFromSegmentArray returns the type string from the segments.
|
|
||||||
func (t *Template) TypeFromSegmentArray(segments []string) string {
|
|
||||||
typeSegment := t.symbolFromSegmentArray("type", segments)
|
|
||||||
|
|
||||||
// Limit type to known types symbols
|
|
||||||
if dns_strings.StringInSlice(typeSegment, types) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Template) symbolFromSegmentArray(symbol string, segments []string) string {
|
|
||||||
index, ok := t.Element[symbol]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return segments[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordNameFromNameValues returns the string produced by applying the
|
|
||||||
// values to the NameTemplate format string.
|
|
||||||
func (t *Template) RecordNameFromNameValues(values NameValues) string {
|
|
||||||
recordName := make([]string, len(t.splitFormat))
|
|
||||||
copy(recordName[:], t.splitFormat)
|
|
||||||
|
|
||||||
for name, index := range t.Element {
|
|
||||||
if index == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch name {
|
|
||||||
case "type":
|
|
||||||
recordName[index] = values.TypeName
|
|
||||||
case "service":
|
|
||||||
recordName[index] = values.ServiceName
|
|
||||||
case "namespace":
|
|
||||||
recordName[index] = values.Namespace
|
|
||||||
case "zone":
|
|
||||||
recordName[index] = values.Zone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(recordName, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid returns true if the template has all the required symbols, false otherwise.
|
|
||||||
func (t *Template) IsValid() bool {
|
|
||||||
result := true
|
|
||||||
|
|
||||||
// Ensure that all requiredSymbols are found in NameTemplate
|
|
||||||
for _, symbol := range requiredSymbols {
|
|
||||||
if _, ok := t.Element[symbol]; !ok {
|
|
||||||
result = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// NameValues contains a number of values.
|
|
||||||
// TODO(...): better docs.
|
|
||||||
type NameValues struct {
|
|
||||||
ServiceName string
|
|
||||||
Namespace string
|
|
||||||
TypeName string
|
|
||||||
Zone string
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
package nametemplate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
zone = 0
|
|
||||||
namespace = 1
|
|
||||||
service = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// Map of format string :: expected locations of name symbols in the format.
|
|
||||||
// -1 value indicates that symbol does not exist in format.
|
|
||||||
var exampleTemplates = map[string][]int{
|
|
||||||
"{service}.{namespace}.{type}.{zone}": {3, 1, 0}, // service symbol expected @ position 0, namespace @ 1, zone @ 3
|
|
||||||
"{namespace}.{type}.{zone}": {2, 0, -1},
|
|
||||||
"": {-1, -1, -1},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetTemplate(t *testing.T) {
|
|
||||||
for s, expectedValue := range exampleTemplates {
|
|
||||||
|
|
||||||
n := new(Template)
|
|
||||||
n.SetTemplate(s)
|
|
||||||
|
|
||||||
// check the indexes resulting from calling SetTemplate() against expectedValues
|
|
||||||
if expectedValue[zone] != -1 {
|
|
||||||
if n.Element["zone"] != expectedValue[zone] {
|
|
||||||
t.Errorf("Expected zone at index '%v', instead found at index '%v' for format string '%v'", expectedValue[zone], n.Element["zone"], s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceFromSegmentArray(t *testing.T) {
|
|
||||||
var (
|
|
||||||
n *Template
|
|
||||||
formatString string
|
|
||||||
queryString string
|
|
||||||
splitQuery []string
|
|
||||||
expectedService string
|
|
||||||
actualService string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Case where template contains {service}
|
|
||||||
n = new(Template)
|
|
||||||
formatString = "{service}.{namespace}.{type}.{zone}"
|
|
||||||
n.SetTemplate(formatString)
|
|
||||||
|
|
||||||
queryString = "myservice.mynamespace.svc.coredns"
|
|
||||||
splitQuery = strings.Split(queryString, ".")
|
|
||||||
expectedService = "myservice"
|
|
||||||
actualService = n.ServiceFromSegmentArray(splitQuery)
|
|
||||||
|
|
||||||
if actualService != expectedService {
|
|
||||||
t.Errorf("Expected service name '%v', instead got service name '%v' for query string '%v' and format '%v'", expectedService, actualService, queryString, formatString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case where template does not contain {service}
|
|
||||||
n = new(Template)
|
|
||||||
formatString = "{namespace}.{type}.{zone}"
|
|
||||||
n.SetTemplate(formatString)
|
|
||||||
|
|
||||||
queryString = "mynamespace.svc.coredns"
|
|
||||||
splitQuery = strings.Split(queryString, ".")
|
|
||||||
expectedService = ""
|
|
||||||
actualService = n.ServiceFromSegmentArray(splitQuery)
|
|
||||||
|
|
||||||
if actualService != expectedService {
|
|
||||||
t.Errorf("Expected service name '%v', instead got service name '%v' for query string '%v' and format '%v'", expectedService, actualService, queryString, formatString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZoneFromSegmentArray(t *testing.T) {
|
|
||||||
var (
|
|
||||||
n *Template
|
|
||||||
formatString string
|
|
||||||
queryString string
|
|
||||||
splitQuery []string
|
|
||||||
expectedZone string
|
|
||||||
actualZone string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Case where template contains {zone}
|
|
||||||
n = new(Template)
|
|
||||||
formatString = "{service}.{namespace}.{type}.{zone}"
|
|
||||||
n.SetTemplate(formatString)
|
|
||||||
|
|
||||||
queryString = "myservice.mynamespace.svc.coredns"
|
|
||||||
splitQuery = strings.Split(queryString, ".")
|
|
||||||
expectedZone = "coredns"
|
|
||||||
actualZone = n.ZoneFromSegmentArray(splitQuery)
|
|
||||||
|
|
||||||
if actualZone != expectedZone {
|
|
||||||
t.Errorf("Expected zone name '%v', instead got zone name '%v' for query string '%v' and format '%v'", expectedZone, actualZone, queryString, formatString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case where template does not contain {zone}
|
|
||||||
n = new(Template)
|
|
||||||
formatString = "{service}.{namespace}.{type}"
|
|
||||||
n.SetTemplate(formatString)
|
|
||||||
|
|
||||||
queryString = "mynamespace.coredns.svc"
|
|
||||||
splitQuery = strings.Split(queryString, ".")
|
|
||||||
expectedZone = ""
|
|
||||||
actualZone = n.ZoneFromSegmentArray(splitQuery)
|
|
||||||
|
|
||||||
if actualZone != expectedZone {
|
|
||||||
t.Errorf("Expected zone name '%v', instead got zone name '%v' for query string '%v' and format '%v'", expectedZone, actualZone, queryString, formatString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case where zone is multiple segments
|
|
||||||
n = new(Template)
|
|
||||||
formatString = "{service}.{namespace}.{type}.{zone}"
|
|
||||||
n.SetTemplate(formatString)
|
|
||||||
|
|
||||||
queryString = "myservice.mynamespace.svc.coredns.cluster.local"
|
|
||||||
splitQuery = strings.Split(queryString, ".")
|
|
||||||
expectedZone = "coredns.cluster.local"
|
|
||||||
actualZone = n.ZoneFromSegmentArray(splitQuery)
|
|
||||||
|
|
||||||
if actualZone != expectedZone {
|
|
||||||
t.Errorf("Expected zone name '%v', instead got zone name '%v' for query string '%v' and format '%v'", expectedZone, actualZone, queryString, formatString)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
"github.com/miekg/coredns/core/dnsserver"
|
"github.com/miekg/coredns/core/dnsserver"
|
||||||
"github.com/miekg/coredns/middleware"
|
"github.com/miekg/coredns/middleware"
|
||||||
"github.com/miekg/coredns/middleware/kubernetes/nametemplate"
|
|
||||||
|
|
||||||
"github.com/mholt/caddy"
|
"github.com/mholt/caddy"
|
||||||
unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned"
|
unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned"
|
||||||
|
@ -52,8 +51,6 @@ func setup(c *caddy.Controller) error {
|
||||||
|
|
||||||
func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
|
func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
|
||||||
k8s := &Kubernetes{ResyncPeriod: defaultResyncPeriod}
|
k8s := &Kubernetes{ResyncPeriod: defaultResyncPeriod}
|
||||||
k8s.NameTemplate = new(nametemplate.Template)
|
|
||||||
k8s.NameTemplate.SetTemplate(defaultNameTemplate)
|
|
||||||
k8s.PodMode = PodModeDisabled
|
k8s.PodMode = PodModeDisabled
|
||||||
|
|
||||||
for c.Next() {
|
for c.Next() {
|
||||||
|
@ -99,18 +96,6 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, c.ArgErr()
|
return nil, c.ArgErr()
|
||||||
|
|
||||||
case "template":
|
|
||||||
args := c.RemainingArgs()
|
|
||||||
if len(args) > 0 {
|
|
||||||
template := strings.Join(args, "")
|
|
||||||
err := k8s.NameTemplate.SetTemplate(template)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, c.ArgErr()
|
|
||||||
case "namespaces":
|
case "namespaces":
|
||||||
args := c.RemainingArgs()
|
args := c.RemainingArgs()
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
|
@ -164,7 +149,6 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultNameTemplate = "{service}.{namespace}.{type}.{zone}"
|
|
||||||
defaultResyncPeriod = 5 * time.Minute
|
defaultResyncPeriod = 5 * time.Minute
|
||||||
defaultPodMode = PodModeDisabled
|
defaultPodMode = PodModeDisabled
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
shouldErr bool // true if test case is exected to produce an error.
|
shouldErr bool // true if test case is exected to produce an error.
|
||||||
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||||
expectedZoneCount int // expected count of defined zones.
|
expectedZoneCount int // expected count of defined zones.
|
||||||
expectedNTValid bool // NameTemplate to be initialized and valid
|
|
||||||
expectedNSCount int // expected count of namespaces.
|
expectedNSCount int // expected count of namespaces.
|
||||||
expectedResyncPeriod time.Duration // expected resync period value
|
expectedResyncPeriod time.Duration // expected resync period value
|
||||||
expectedLabelSelector string // expected label selector value
|
expectedLabelSelector string // expected label selector value
|
||||||
|
@ -28,7 +27,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -39,7 +37,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
2,
|
2,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -51,7 +48,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -64,20 +60,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
0,
|
|
||||||
defaultResyncPeriod,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"template keyword with valid template",
|
|
||||||
`kubernetes coredns.local {
|
|
||||||
template {service}.{namespace}.{zone}
|
|
||||||
}`,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -90,7 +72,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
1,
|
1,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -103,7 +84,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
2,
|
2,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -116,7 +96,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
30 * time.Second,
|
30 * time.Second,
|
||||||
"",
|
"",
|
||||||
|
@ -129,7 +108,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
15 * time.Minute,
|
15 * time.Minute,
|
||||||
"",
|
"",
|
||||||
|
@ -142,7 +120,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"environment=prod",
|
"environment=prod",
|
||||||
|
@ -155,7 +132,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
1,
|
1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"application=nginx,environment in (production,qa,staging)",
|
"application=nginx,environment in (production,qa,staging)",
|
||||||
|
@ -165,14 +141,12 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
`kubernetes coredns.local test.local {
|
`kubernetes coredns.local test.local {
|
||||||
resyncperiod 15m
|
resyncperiod 15m
|
||||||
endpoint http://localhost:8080
|
endpoint http://localhost:8080
|
||||||
template {service}.{namespace}.{zone}
|
|
||||||
namespaces demo test
|
namespaces demo test
|
||||||
labels environment in (production, staging, qa),application=nginx
|
labels environment in (production, staging, qa),application=nginx
|
||||||
}`,
|
}`,
|
||||||
false,
|
false,
|
||||||
"",
|
"",
|
||||||
2,
|
2,
|
||||||
true,
|
|
||||||
2,
|
2,
|
||||||
15 * time.Minute,
|
15 * time.Minute,
|
||||||
"application=nginx,environment in (production,qa,staging)",
|
"application=nginx,environment in (production,qa,staging)",
|
||||||
|
@ -184,7 +158,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Kubernetes setup called without keyword 'kubernetes' in Corefile",
|
"Kubernetes setup called without keyword 'kubernetes' in Corefile",
|
||||||
-1,
|
-1,
|
||||||
false,
|
|
||||||
-1,
|
-1,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -195,7 +168,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Zone name must be provided for kubernetes middleware",
|
"Zone name must be provided for kubernetes middleware",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -208,37 +180,10 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Wrong argument count or unexpected line ending after 'endpoint'",
|
"Wrong argument count or unexpected line ending after 'endpoint'",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
-1,
|
-1,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"template keyword without a template value",
|
|
||||||
`kubernetes coredns.local {
|
|
||||||
template
|
|
||||||
}`,
|
|
||||||
true,
|
|
||||||
"Wrong argument count or unexpected line ending after 'template'",
|
|
||||||
-1,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
defaultResyncPeriod,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"template keyword with an invalid template value",
|
|
||||||
`kubernetes coredns.local {
|
|
||||||
template {namespace}.{zone}
|
|
||||||
}`,
|
|
||||||
true,
|
|
||||||
"Record name template does not pass NameTemplate validation",
|
|
||||||
-1,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
defaultResyncPeriod,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"namespace keyword without a namespace value",
|
"namespace keyword without a namespace value",
|
||||||
`kubernetes coredns.local {
|
`kubernetes coredns.local {
|
||||||
|
@ -247,7 +192,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Parse error: Wrong argument count or unexpected line ending after 'namespaces'",
|
"Parse error: Wrong argument count or unexpected line ending after 'namespaces'",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
-1,
|
-1,
|
||||||
defaultResyncPeriod,
|
defaultResyncPeriod,
|
||||||
"",
|
"",
|
||||||
|
@ -260,7 +204,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Wrong argument count or unexpected line ending after 'resyncperiod'",
|
"Wrong argument count or unexpected line ending after 'resyncperiod'",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
0 * time.Minute,
|
0 * time.Minute,
|
||||||
"",
|
"",
|
||||||
|
@ -273,7 +216,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Unable to parse resync duration value. Value provided was ",
|
"Unable to parse resync duration value. Value provided was ",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
0 * time.Second,
|
0 * time.Second,
|
||||||
"",
|
"",
|
||||||
|
@ -286,7 +228,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Unable to parse resync duration value. Value provided was ",
|
"Unable to parse resync duration value. Value provided was ",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
0 * time.Second,
|
0 * time.Second,
|
||||||
"",
|
"",
|
||||||
|
@ -299,7 +240,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Wrong argument count or unexpected line ending after 'labels'",
|
"Wrong argument count or unexpected line ending after 'labels'",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
0 * time.Second,
|
0 * time.Second,
|
||||||
"",
|
"",
|
||||||
|
@ -312,7 +252,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
true,
|
true,
|
||||||
"Unable to parse label selector. Value provided was",
|
"Unable to parse label selector. Value provided was",
|
||||||
-1,
|
-1,
|
||||||
true,
|
|
||||||
0,
|
0,
|
||||||
0 * time.Second,
|
0 * time.Second,
|
||||||
"",
|
"",
|
||||||
|
@ -354,16 +293,6 @@ func TestKubernetesParse(t *testing.T) {
|
||||||
t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d zones, instead found %d zones: '%v' for input '%s'", i, test.expectedZoneCount, foundZoneCount, k8sController.Zones, test.input)
|
t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d zones, instead found %d zones: '%v' for input '%s'", i, test.expectedZoneCount, foundZoneCount, k8sController.Zones, test.input)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NameTemplate
|
|
||||||
if k8sController.NameTemplate == nil {
|
|
||||||
t.Errorf("Test %d: Expected kubernetes controller to be initialized with a NameTemplate. Instead found '%v' for input '%s'", i, k8sController.NameTemplate, test.input)
|
|
||||||
} else {
|
|
||||||
foundNTValid := k8sController.NameTemplate.IsValid()
|
|
||||||
if foundNTValid != test.expectedNTValid {
|
|
||||||
t.Errorf("Test %d: Expected NameTemplate validity to be '%v', instead found '%v' for input '%s'", i, test.expectedNTValid, foundNTValid, test.input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Namespaces
|
// Namespaces
|
||||||
foundNSCount := len(k8sController.Namespaces)
|
foundNSCount := len(k8sController.Namespaces)
|
||||||
if foundNSCount != test.expectedNSCount {
|
if foundNSCount != test.expectedNSCount {
|
||||||
|
|
|
@ -105,7 +105,7 @@ var dnsTestCases = []test.Case{
|
||||||
},
|
},
|
||||||
//TODO: Fix below to all use test.SRV not test.A!
|
//TODO: Fix below to all use test.SRV not test.A!
|
||||||
{
|
{
|
||||||
Qname: "_*._*.svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*._TcP.svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeSuccess,
|
Rcode: dns.RcodeSuccess,
|
||||||
Answer: []dns.RR{
|
Answer: []dns.RR{
|
||||||
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
||||||
|
@ -113,12 +113,12 @@ var dnsTestCases = []test.Case{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._*.bogusservice.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*.*.bogusservice.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeNameError,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._*.svc-1-a.*.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*.any.svc-1-a.*.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeSuccess,
|
Rcode: dns.RcodeSuccess,
|
||||||
Answer: []dns.RR{
|
Answer: []dns.RR{
|
||||||
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
||||||
|
@ -126,7 +126,7 @@ var dnsTestCases = []test.Case{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._*.svc-1-a.any.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "ANY.*.svc-1-a.any.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeSuccess,
|
Rcode: dns.RcodeSuccess,
|
||||||
Answer: []dns.RR{
|
Answer: []dns.RR{
|
||||||
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
||||||
|
@ -134,17 +134,17 @@ var dnsTestCases = []test.Case{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._*.bogusservice.*.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*.*.bogusservice.*.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeNameError,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._*.bogusservice.any.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*.*.bogusservice.any.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeNameError,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_c-port._*.*.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "_c-port._UDP.*.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeSuccess,
|
Rcode: dns.RcodeSuccess,
|
||||||
Answer: []dns.RR{
|
Answer: []dns.RR{
|
||||||
test.SRV("_c-port._udp.svc-c.test-1.svc.cluster.local. 303 IN SRV 10 100 1234 svc-c.test-1.svc.cluster.local."),
|
test.SRV("_c-port._udp.svc-c.test-1.svc.cluster.local. 303 IN SRV 10 100 1234 svc-c.test-1.svc.cluster.local."),
|
||||||
|
@ -153,7 +153,7 @@ var dnsTestCases = []test.Case{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._tcp.any.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*._tcp.any.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeSuccess,
|
Rcode: dns.RcodeSuccess,
|
||||||
Answer: []dns.RR{
|
Answer: []dns.RR{
|
||||||
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
test.SRV("_http._tcp.svc-1-a.test-1.svc.cluster.local. 303 IN SRV 10 100 80 svc-1-a.test-1.svc.cluster.local."),
|
||||||
|
@ -162,12 +162,12 @@ var dnsTestCases = []test.Case{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._*.any.test-2.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*.*.any.test-2.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeNameError,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._*.*.test-2.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*.*.*.test-2.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeNameError,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
|
@ -180,18 +180,18 @@ var dnsTestCases = []test.Case{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*.svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*.svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeServerFailure,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "_*._not-udp-or-tcp.svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "*._not-udp-or-tcp.svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeServerFailure,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qname: "svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
Qname: "svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeSRV,
|
||||||
Rcode: dns.RcodeNameError,
|
Rcode: dns.RcodeServerFailure,
|
||||||
Answer: []dns.RR{},
|
Answer: []dns.RR{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue