aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--conf/k8sCorefile7
-rw-r--r--core/setup/kubernetes.go15
-rw-r--r--core/setup/kubernetes_test.go101
-rw-r--r--middleware/kubernetes/README.md31
-rw-r--r--middleware/kubernetes/controller.go49
-rw-r--r--middleware/kubernetes/kubernetes.go38
6 files changed, 194 insertions, 47 deletions
diff --git a/conf/k8sCorefile b/conf/k8sCorefile
index ca05461b0..96b48f2fb 100644
--- a/conf/k8sCorefile
+++ b/conf/k8sCorefile
@@ -11,6 +11,13 @@
template {service}.{namespace}.{zone}
# Only expose the k8s namespace "demo"
namespaces demo
+ # Only expose the records for kubernetes objects
+ # that matches this label selector. The label
+ # selector syntax is described in the kubernetes
+ # API documentation: http://kubernetes.io/docs/user-guide/labels/
+ # Example selector below only exposes objects tagged as
+ # "application=nginx" in the staging or qa environments.
+ #labels environment in (staging, qa),application=nginx
}
# Perform DNS response caching for the coredns.local zone
# Cache timeout is provided by the integer in seconds
diff --git a/core/setup/kubernetes.go b/core/setup/kubernetes.go
index 17b37e00e..7439a9f1b 100644
--- a/core/setup/kubernetes.go
+++ b/core/setup/kubernetes.go
@@ -10,6 +10,7 @@ import (
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/kubernetes"
"github.com/miekg/coredns/middleware/kubernetes/nametemplate"
+ unversionedapi "k8s.io/kubernetes/pkg/api/unversioned"
)
const (
@@ -109,6 +110,20 @@ func kubernetesParse(c *Controller) (kubernetes.Kubernetes, error) {
log.Printf("[debug] 'resyncperiod' keyword provided without any duration value.")
return kubernetes.Kubernetes{}, c.ArgErr()
}
+ case "labels":
+ args := c.RemainingArgs()
+ if len(args) != 0 {
+ labelSelectorString := strings.Join(args, " ")
+ k8s.LabelSelector, err = unversionedapi.ParseToLabelSelector(labelSelectorString)
+ if err != nil {
+ err = errors.New(fmt.Sprintf("Unable to parse label selector. Value provided was '%v'. Error was: %v", labelSelectorString, err))
+ log.Printf("[ERROR] %v", err)
+ return kubernetes.Kubernetes{}, err
+ }
+ } else {
+ log.Printf("[debug] 'labels' keyword provided without any selector value.")
+ return kubernetes.Kubernetes{}, c.ArgErr()
+ }
}
}
return k8s, nil
diff --git a/core/setup/kubernetes_test.go b/core/setup/kubernetes_test.go
index 4674896b8..cf6ac9abc 100644
--- a/core/setup/kubernetes_test.go
+++ b/core/setup/kubernetes_test.go
@@ -4,18 +4,21 @@ import (
"strings"
"testing"
"time"
+
+ unversionedapi "k8s.io/kubernetes/pkg/api/unversioned"
)
func TestKubernetesParse(t *testing.T) {
tests := []struct {
- description string // Human-facing description of test case
- input string // Corefile data as string
- shouldErr bool // true if test case is exected to produce an error.
- expectedErrContent string // substring from the expected error. Empty for positive cases.
- expectedZoneCount int // expected count of defined zones.
- expectedNTValid bool // NameTemplate to be initialized and valid
- expectedNSCount int // expected count of namespaces.
- expectedResyncPeriod time.Duration // expected resync period value
+ description string // Human-facing description of test case
+ input string // Corefile data as string
+ shouldErr bool // true if test case is exected to produce an error.
+ expectedErrContent string // substring from the expected error. Empty for positive cases.
+ expectedZoneCount int // expected count of defined zones.
+ expectedNTValid bool // NameTemplate to be initialized and valid
+ expectedNSCount int // expected count of namespaces.
+ expectedResyncPeriod time.Duration // expected resync period value
+ expectedLabelSelector string // expected label selector value
}{
// positive
{
@@ -27,6 +30,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
defaultResyncPeriod,
+ "",
},
{
"kubernetes keyword with multiple zones",
@@ -37,6 +41,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
defaultResyncPeriod,
+ "",
},
{
"kubernetes keyword with zone and empty braces",
@@ -48,6 +53,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
defaultResyncPeriod,
+ "",
},
{
"endpoint keyword with url",
@@ -60,6 +66,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
defaultResyncPeriod,
+ "",
},
{
"template keyword with valid template",
@@ -72,6 +79,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
defaultResyncPeriod,
+ "",
},
{
"namespaces keyword with one namespace",
@@ -84,6 +92,7 @@ func TestKubernetesParse(t *testing.T) {
true,
1,
defaultResyncPeriod,
+ "",
},
{
"namespaces keyword with multiple namespaces",
@@ -96,6 +105,7 @@ func TestKubernetesParse(t *testing.T) {
true,
2,
defaultResyncPeriod,
+ "",
},
{
"resync period in seconds",
@@ -108,6 +118,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
30 * time.Second,
+ "",
},
{
"resync period in minutes",
@@ -120,6 +131,33 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
15 * time.Minute,
+ "",
+ },
+ {
+ "basic label selector",
+ `kubernetes coredns.local {
+ labels environment=prod
+}`,
+ false,
+ "",
+ 1,
+ true,
+ 0,
+ defaultResyncPeriod,
+ "environment=prod",
+ },
+ {
+ "multi-label selector",
+ `kubernetes coredns.local {
+ labels environment in (production, staging, qa),application=nginx
+}`,
+ false,
+ "",
+ 1,
+ true,
+ 0,
+ defaultResyncPeriod,
+ "application=nginx,environment in (production,qa,staging)",
},
{
"fully specified valid config",
@@ -128,6 +166,7 @@ func TestKubernetesParse(t *testing.T) {
endpoint http://localhost:8080
template {service}.{namespace}.{zone}
namespaces demo test
+ labels environment in (production, staging, qa),application=nginx
}`,
false,
"",
@@ -135,6 +174,7 @@ func TestKubernetesParse(t *testing.T) {
true,
2,
15 * time.Minute,
+ "application=nginx,environment in (production,qa,staging)",
},
// negative
{
@@ -146,6 +186,7 @@ func TestKubernetesParse(t *testing.T) {
false,
-1,
defaultResyncPeriod,
+ "",
},
{
"kubernetes keyword without a zone",
@@ -156,6 +197,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
defaultResyncPeriod,
+ "",
},
{
"endpoint keyword without an endpoint value",
@@ -168,6 +210,7 @@ func TestKubernetesParse(t *testing.T) {
true,
-1,
defaultResyncPeriod,
+ "",
},
{
"template keyword without a template value",
@@ -180,6 +223,7 @@ func TestKubernetesParse(t *testing.T) {
false,
0,
defaultResyncPeriod,
+ "",
},
{
"template keyword with an invalid template value",
@@ -192,6 +236,7 @@ func TestKubernetesParse(t *testing.T) {
false,
0,
defaultResyncPeriod,
+ "",
},
{
"namespace keyword without a namespace value",
@@ -204,6 +249,7 @@ func TestKubernetesParse(t *testing.T) {
true,
-1,
defaultResyncPeriod,
+ "",
},
{
"resyncperiod keyword without a duration value",
@@ -216,6 +262,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
0 * time.Minute,
+ "",
},
{
"resync period no units",
@@ -228,6 +275,7 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
0 * time.Second,
+ "",
},
{
"resync period invalid",
@@ -240,6 +288,33 @@ func TestKubernetesParse(t *testing.T) {
true,
0,
0 * time.Second,
+ "",
+ },
+ {
+ "labels with no selector value",
+ `kubernetes coredns.local {
+ labels
+}`,
+ true,
+ "Wrong argument count or unexpected line ending after 'labels'",
+ -1,
+ true,
+ 0,
+ 0 * time.Second,
+ "",
+ },
+ {
+ "labels with invalid selector value",
+ `kubernetes coredns.local {
+ labels environment in (production, qa
+}`,
+ true,
+ "Unable to parse label selector. Value provided was",
+ -1,
+ true,
+ 0,
+ 0 * time.Second,
+ "",
},
}
@@ -300,7 +375,15 @@ func TestKubernetesParse(t *testing.T) {
// ResyncPeriod
foundResyncPeriod := k8sController.ResyncPeriod
if foundResyncPeriod != test.expectedResyncPeriod {
- t.Errorf("Test %d: Expected kubernetes controller to be initialized with resync period '%s'. Instead found period '%s' for input '%s'", test.expectedResyncPeriod, foundResyncPeriod, test.input)
+ t.Errorf("Test %d: Expected kubernetes controller to be initialized with resync period '%s'. Instead found period '%s' for input '%s'", i, test.expectedResyncPeriod, foundResyncPeriod, test.input)
+ }
+
+ // Labels
+ if k8sController.LabelSelector != nil {
+ foundLabelSelectorString := unversionedapi.FormatLabelSelector(k8sController.LabelSelector)
+ if foundLabelSelectorString != test.expectedLabelSelector {
+ t.Errorf("Test %d: Expected kubernetes controller to be initialized with label selector '%s'. Instead found selector '%s' for input '%s'", i, test.expectedLabelSelector, foundLabelSelectorString, test.input)
+ }
}
}
}
diff --git a/middleware/kubernetes/README.md b/middleware/kubernetes/README.md
index 0ae9681d4..024e448db 100644
--- a/middleware/kubernetes/README.md
+++ b/middleware/kubernetes/README.md
@@ -44,6 +44,13 @@ This is the default kubernetes setup, with everything specified in full:
template {service}.{namespace}.{zone}
# Only expose the k8s namespace "demo"
namespaces demo
+ # Only expose the records for kubernetes objects
+ # that matches this label selector. The label
+ # selector syntax is described in the kubernetes
+ # API documentation: http://kubernetes.io/docs/user-guide/labels/
+ # Example selector below only exposes objects tagged as
+ # "application=nginx" in the staging or qa environments.
+ #labels environment in (staging, qa),application=nginx
}
# Perform DNS response caching for the coredns.local zone
# Cache timeout is provided by the integer in seconds
@@ -51,10 +58,13 @@ This is the default kubernetes setup, with everything specified in full:
}
~~~
-Notes:
+Defaults:
* If the `namespaces` keyword is omitted, all kubernetes namespaces are exposed.
* If the `template` keyword is omitted, the default template of "{service}.{namespace}.{zone}" is used.
* If the `resyncperiod` keyword is omitted, the default resync period is 5 minutes.
+* The `labels` keyword is only used when filtering of results based on kubernetes label selector syntax
+ is required. The label selector syntax is described in the kubernetes API documentation at:
+ http://kubernetes.io/docs/user-guide/labels/
### Basic Setup
@@ -191,7 +201,7 @@ mynginx.demo.coredns.local. 0 IN A 10.0.0.10
## Implementation Notes/Ideas
-### Basic Zone Mapping (implemented)
+### Basic Zone Mapping
The middleware is configured with a "zone" string. For
example: "zone = coredns.local".
@@ -200,8 +210,8 @@ to: "myservice.mynamespace.coredns.local".
The middleware should publish an A record for that service and a service record.
-Initial implementation just performs the above simple mapping. Subsequent
-revisions should allow different namespaces to be published under different zones.
+If multiple zone names are specified, the records for kubernetes objects are
+exposed in all listed zones.
For example:
@@ -262,11 +272,6 @@ return the IP addresses for all services with "nginx" in the service name.
TBD:
* How does this relate the the k8s load-balancer configuration?
-* Do wildcards search across namespaces? (Yes)
-* Initial implementation assumes that a namespace maps to the first DNS label
- below the zone managed by the kubernetes middleware. This assumption may
- need to be revised. (Template scheme for record names removes this assumption.)
-
## TODO
* SkyDNS compatibility/equivalency:
@@ -318,19 +323,19 @@ TBD:
* Additional features:
* Reverse IN-ADDR entries for services. (Is there any value in supporting
reverse lookup records?) (need tests, functionality should work based on @aledbf's code.)
- * How to support label specification in Corefile to allow use of labels to
- indicate zone? (Is this even useful?) For example, the following
+ * (done) ~~How to support label specification in Corefile to allow use of labels to
+ indicate zone? For example, the following
configuration exposes all services labeled for the "staging" environment
and tenant "customerB" in the zone "customerB.stage.local":
kubernetes customerB.stage.local {
# Use url for k8s API endpoint
endpoint http://localhost:8080
- label "environment" : "staging", "tenant" : "customerB"
+ labels environment in (staging),tenant=customerB
}
Note: label specification/selection is a killer feature for segmenting
- test vs staging vs prod environments.
+ test vs staging vs prod environments.~~ Need label testing.
* Implement IP selection and ordering (internal/external). Related to
wildcards and SkyDNS use of CNAMES.
* Flatten service and namespace names to valid DNS characters. (service names
diff --git a/middleware/kubernetes/controller.go b/middleware/kubernetes/controller.go
index 3dc88d2f1..3fbea313e 100644
--- a/middleware/kubernetes/controller.go
+++ b/middleware/kubernetes/controller.go
@@ -12,6 +12,7 @@ import (
"k8s.io/kubernetes/pkg/client/cache"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/controller/framework"
+ "k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/watch"
)
@@ -23,6 +24,8 @@ var (
type dnsController struct {
client *client.Client
+ selector *labels.Selector
+
endpController *framework.Controller
svcController *framework.Controller
nsController *framework.Controller
@@ -40,68 +43,87 @@ type dnsController struct {
}
// newDNSController creates a controller for coredns
-func newdnsController(kubeClient *client.Client, resyncPeriod time.Duration) *dnsController {
+func newdnsController(kubeClient *client.Client, resyncPeriod time.Duration, lselector *labels.Selector) *dnsController {
dns := dnsController{
client: kubeClient,
+ selector: lselector,
stopCh: make(chan struct{}),
}
dns.endpLister.Store, dns.endpController = framework.NewInformer(
&cache.ListWatch{
- ListFunc: endpointsListFunc(dns.client, namespace),
- WatchFunc: endpointsWatchFunc(dns.client, namespace),
+ ListFunc: endpointsListFunc(dns.client, namespace, dns.selector),
+ WatchFunc: endpointsWatchFunc(dns.client, namespace, dns.selector),
},
&api.Endpoints{}, resyncPeriod, framework.ResourceEventHandlerFuncs{})
dns.svcLister.Store, dns.svcController = framework.NewInformer(
&cache.ListWatch{
- ListFunc: serviceListFunc(dns.client, namespace),
- WatchFunc: serviceWatchFunc(dns.client, namespace),
+ ListFunc: serviceListFunc(dns.client, namespace, dns.selector),
+ WatchFunc: serviceWatchFunc(dns.client, namespace, dns.selector),
},
&api.Service{}, resyncPeriod, framework.ResourceEventHandlerFuncs{})
dns.nsLister.Store, dns.nsController = framework.NewInformer(
&cache.ListWatch{
- ListFunc: namespaceListFunc(dns.client),
- WatchFunc: namespaceWatchFunc(dns.client),
+ ListFunc: namespaceListFunc(dns.client, dns.selector),
+ WatchFunc: namespaceWatchFunc(dns.client, dns.selector),
},
&api.Namespace{}, resyncPeriod, framework.ResourceEventHandlerFuncs{})
return &dns
}
-func serviceListFunc(c *client.Client, ns string) func(api.ListOptions) (runtime.Object, error) {
+func serviceListFunc(c *client.Client, ns string, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) {
return func(opts api.ListOptions) (runtime.Object, error) {
+ if s != nil {
+ opts.LabelSelector = *s
+ }
return c.Services(ns).List(opts)
}
}
-func serviceWatchFunc(c *client.Client, ns string) func(options api.ListOptions) (watch.Interface, error) {
+func serviceWatchFunc(c *client.Client, ns string, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) {
return func(options api.ListOptions) (watch.Interface, error) {
+ if s != nil {
+ options.LabelSelector = *s
+ }
return c.Services(ns).Watch(options)
}
}
-func endpointsListFunc(c *client.Client, ns string) func(api.ListOptions) (runtime.Object, error) {
+func endpointsListFunc(c *client.Client, ns string, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) {
return func(opts api.ListOptions) (runtime.Object, error) {
+ if s != nil {
+ opts.LabelSelector = *s
+ }
return c.Endpoints(ns).List(opts)
}
}
-func endpointsWatchFunc(c *client.Client, ns string) func(options api.ListOptions) (watch.Interface, error) {
+func endpointsWatchFunc(c *client.Client, ns string, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) {
return func(options api.ListOptions) (watch.Interface, error) {
+ if s != nil {
+ options.LabelSelector = *s
+ }
return c.Endpoints(ns).Watch(options)
}
}
-func namespaceListFunc(c *client.Client) func(api.ListOptions) (runtime.Object, error) {
+func namespaceListFunc(c *client.Client, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) {
return func(opts api.ListOptions) (runtime.Object, error) {
+ if s != nil {
+ opts.LabelSelector = *s
+ }
return c.Namespaces().List(opts)
}
}
-func namespaceWatchFunc(c *client.Client) func(options api.ListOptions) (watch.Interface, error) {
+func namespaceWatchFunc(c *client.Client, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) {
return func(options api.ListOptions) (watch.Interface, error) {
+ if s != nil {
+ options.LabelSelector = *s
+ }
return c.Namespaces().Watch(options)
}
}
@@ -149,7 +171,6 @@ func (dns *dnsController) GetNamespaceList() *api.NamespaceList {
}
func (dns *dnsController) GetServiceList() *api.ServiceList {
- log.Printf("[debug] here in GetServiceList")
svcList, err := dns.svcLister.List()
if err != nil {
return &api.ServiceList{}
diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go
index 7c64580c1..4fa1e494b 100644
--- a/middleware/kubernetes/kubernetes.go
+++ b/middleware/kubernetes/kubernetes.go
@@ -15,20 +15,24 @@ import (
"github.com/miekg/dns"
"k8s.io/kubernetes/pkg/api"
- "k8s.io/kubernetes/pkg/client/unversioned"
+ unversionedapi "k8s.io/kubernetes/pkg/api/unversioned"
+ "k8s.io/kubernetes/pkg/labels"
+ unversionedclient "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
)
type Kubernetes struct {
- Next middleware.Handler
- Zones []string
- Proxy proxy.Proxy // Proxy for looking up names during the resolution process
- APIEndpoint string
- APIConn *dnsController
- ResyncPeriod time.Duration
- NameTemplate *nametemplate.NameTemplate
- Namespaces []string
+ Next middleware.Handler
+ Zones []string
+ Proxy proxy.Proxy // Proxy for looking up names during the resolution process
+ APIEndpoint string
+ APIConn *dnsController
+ ResyncPeriod time.Duration
+ NameTemplate *nametemplate.NameTemplate
+ Namespaces []string
+ LabelSelector *unversionedapi.LabelSelector
+ Selector *labels.Selector
}
func (g *Kubernetes) StartKubeCache() error {
@@ -45,14 +49,26 @@ func (g *Kubernetes) StartKubeCache() error {
log.Printf("[debug] error connecting to the client: %v", err)
return err
}
- kubeClient, err := unversioned.New(config)
+ kubeClient, err := unversionedclient.New(config)
if err != nil {
log.Printf("[ERROR] Failed to create kubernetes notification controller: %v", err)
return err
}
+ if g.LabelSelector == nil {
+ log.Printf("[INFO] Kubernetes middleware configured without a label selector. No label-based filtering will be operformed.")
+ } else {
+ var selector labels.Selector
+ selector, err = unversionedapi.LabelSelectorAsSelector(g.LabelSelector)
+ g.Selector = &selector
+ if err != nil {
+ log.Printf("[ERROR] Unable to create Selector for LabelSelector '%s'.Error was: %s", g.LabelSelector, err)
+ return err
+ }
+ log.Printf("[INFO] Kubernetes middleware configured with the label selector '%s'. Only kubernetes objects matching this label selector will be exposed.", unversionedapi.FormatLabelSelector(g.LabelSelector))
+ }
log.Printf("[debug] Starting kubernetes middleware with k8s API resync period: %s", g.ResyncPeriod)
- g.APIConn = newdnsController(kubeClient, g.ResyncPeriod)
+ g.APIConn = newdnsController(kubeClient, g.ResyncPeriod, g.Selector)
go g.APIConn.Run()