implemented annotationFilter for node source

This commit is contained in:
Reinier Schoof 2019-10-01 11:26:47 +02:00
parent a491d8f6a2
commit 16194ca9cf
3 changed files with 130 additions and 32 deletions

View File

@ -25,6 +25,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
@ -36,12 +37,13 @@ import (
) )
type nodeSource struct { type nodeSource struct {
client kubernetes.Interface client kubernetes.Interface
fqdnTemplate *template.Template annotationFilter string
nodeInformer coreinformers.NodeInformer fqdnTemplate *template.Template
nodeInformer coreinformers.NodeInformer
} }
func NewNodeSource(kubeClient kubernetes.Interface, fqdnTemplate string) (Source, error) { func NewNodeSource(kubeClient kubernetes.Interface, annotationFilter, fqdnTemplate string) (Source, error) {
var ( var (
tmpl *template.Template tmpl *template.Template
err error err error
@ -82,9 +84,10 @@ func NewNodeSource(kubeClient kubernetes.Interface, fqdnTemplate string) (Source
} }
return &nodeSource{ return &nodeSource{
client: kubeClient, client: kubeClient,
fqdnTemplate: tmpl, annotationFilter: annotationFilter,
nodeInformer: nodeInformer, fqdnTemplate: tmpl,
nodeInformer: nodeInformer,
}, nil }, nil
} }
@ -95,6 +98,11 @@ func (ns *nodeSource) Endpoints() ([]*endpoint.Endpoint, error) {
return nil, err return nil, err
} }
nodes, err = ns.filterByAnnotations(nodes)
if err != nil {
return nil, err
}
endpoints := []*endpoint.Endpoint{} endpoints := []*endpoint.Endpoint{}
// create endpoints for all nodes // create endpoints for all nodes
@ -149,3 +157,34 @@ func (ns *nodeSource) nodeAddress(node *v1.Node) (string, error) {
return "", fmt.Errorf("could not find node address for %s", node.Name) return "", fmt.Errorf("could not find node address for %s", node.Name)
} }
// filterByAnnotations filters a list of nodes by a given annotation selector.
func (ns *nodeSource) filterByAnnotations(nodes []*v1.Node) ([]*v1.Node, error) {
labelSelector, err := metav1.ParseToLabelSelector(ns.annotationFilter)
if err != nil {
return nil, err
}
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil {
return nil, err
}
// empty filter returns original list
if selector.Empty() {
return nodes, nil
}
filteredList := []*v1.Node{}
for _, node := range nodes {
// convert the node's annotations to an equivalent label selector
annotations := labels.Set(node.Annotations)
// include node if its annotations match the selector
if selector.Matches(annotations) {
filteredList = append(filteredList, node)
}
}
return filteredList, nil
}

View File

@ -14,16 +14,17 @@ import (
func TestNodeSource(t *testing.T) { func TestNodeSource(t *testing.T) {
//suite.Run(t, new(ServiceSuite)) //suite.Run(t, new(ServiceSuite))
//t.Run("Interface", testServiceSourceImplementsSource) //t.Run("Interface", testServiceSourceImplementsSource)
//t.Run("NewNodeSource", testNodeSourceNewNodeSource) t.Run("NewNodeSource", testNodeSourceNewNodeSource)
t.Run("Endpoints", testNodeSourceEndpoints) t.Run("Endpoints", testNodeSourceEndpoints)
} }
// testNodeSourceNewNodeSource tests that NewNodeService doesn't return an error. // testNodeSourceNewNodeSource tests that NewNodeService doesn't return an error.
func testNodeSourceNewNodeSource(t *testing.T) { func testNodeSourceNewNodeSource(t *testing.T) {
for _, ti := range []struct { for _, ti := range []struct {
title string title string
fqdnTemplate string annotationFilter string
expectError bool fqdnTemplate string
expectError bool
}{ }{
{ {
title: "invalid template", title: "invalid template",
@ -39,10 +40,16 @@ func testNodeSourceNewNodeSource(t *testing.T) {
expectError: false, expectError: false,
fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com", fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com",
}, },
{
title: "non-empty annotation filter label",
expectError: false,
annotationFilter: "kubernetes.io/ingress.class=nginx",
},
} { } {
t.Run(ti.title, func(t *testing.T) { t.Run(ti.title, func(t *testing.T) {
_, err := NewNodeSource( _, err := NewNodeSource(
fake.NewSimpleClientset(), fake.NewSimpleClientset(),
ti.annotationFilter,
ti.fqdnTemplate, ti.fqdnTemplate,
) )
@ -58,66 +65,72 @@ func testNodeSourceNewNodeSource(t *testing.T) {
// testNodeSourceEndpoints tests that various node generate the correct endpoints. // testNodeSourceEndpoints tests that various node generate the correct endpoints.
func testNodeSourceEndpoints(t *testing.T) { func testNodeSourceEndpoints(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
title string title string
fqdnTemplate string annotationFilter string
nodeName string fqdnTemplate string
nodeAddresses []v1.NodeAddress nodeName string
labels map[string]string nodeAddresses []v1.NodeAddress
annotations map[string]string labels map[string]string
expected []*endpoint.Endpoint annotations map[string]string
expectError bool expected []*endpoint.Endpoint
expectError bool
}{ }{
{ {
"node with short hostname returns one endpoint", "node with short hostname returns one endpoint",
"", "",
"",
"node1", "node1",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}}, []v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}},
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}}, {RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
}, },
false, false,
}, },
{ {
"node with fqdn returns one endpoint", "node with fqdn returns one endpoint",
"", "",
"",
"node1.example.org", "node1.example.org",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}}, []v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}},
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
}, },
false, false,
}, },
{ {
"node with fqdn template returns endpoint with expanded hostname", "node with fqdn template returns endpoint with expanded hostname",
"",
"{{.Name}}.example.org", "{{.Name}}.example.org",
"node1", "node1",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}}, []v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}},
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
}, },
false, false,
}, },
{ {
"node with fqdn and fqdn template returns one endpoint", "node with fqdn and fqdn template returns one endpoint",
"",
"{{.Name}}.example.org", "{{.Name}}.example.org",
"node1.example.org", "node1.example.org",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}}, []v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}},
map[string]string{}, map[string]string{},
map[string]string{}, map[string]string{},
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1.example.org.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {RecordType: "A", DNSName: "node1.example.org.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
}, },
false, false,
}, },
{ {
"node with both external and internal IP returns an endpoint with external IP", "node with both external and internal IP returns an endpoint with external IP",
"", "",
"",
"node1", "node1",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}, {v1.NodeInternalIP, "2.3.4.5"}}, []v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}, {v1.NodeInternalIP, "2.3.4.5"}},
map[string]string{}, map[string]string{},
@ -127,9 +140,10 @@ func testNodeSourceEndpoints(t *testing.T) {
}, },
false, false,
}, },
{ {
"node with only internal IP returns an endpoint with internal IP", "node with only internal IP returns an endpoint with internal IP",
"", "",
"",
"node1", "node1",
[]v1.NodeAddress{{v1.NodeInternalIP, "2.3.4.5"}}, []v1.NodeAddress{{v1.NodeInternalIP, "2.3.4.5"}},
map[string]string{}, map[string]string{},
@ -139,9 +153,10 @@ func testNodeSourceEndpoints(t *testing.T) {
}, },
false, false,
}, },
{ {
"node with neither external nor internal IP returns no endpoints", "node with neither external nor internal IP returns no endpoints",
"", "",
"",
"node1", "node1",
[]v1.NodeAddress{}, []v1.NodeAddress{},
map[string]string{}, map[string]string{},
@ -149,6 +164,49 @@ func testNodeSourceEndpoints(t *testing.T) {
[]*endpoint.Endpoint{}, []*endpoint.Endpoint{},
false, false,
}, },
{
"annotated node without annotation filter returns endpoint",
"",
"",
"node1",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}},
map[string]string{},
map[string]string{
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
[]*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
{
"annotated node with matching annotation filter returns endpoint",
"service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
"",
"node1",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}},
map[string]string{},
map[string]string{
"service.beta.kubernetes.io/external-traffic": "OnlyLocal",
},
[]*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1", Targets: endpoint.Targets{"1.2.3.4"}},
},
false,
},
{
"annotated node with non-matching annotation filter returns endpoint",
"service.beta.kubernetes.io/external-traffic in (Global, OnlyLocal)",
"",
"node1",
[]v1.NodeAddress{{v1.NodeExternalIP, "1.2.3.4"}},
map[string]string{},
map[string]string{
"service.beta.kubernetes.io/external-traffic": "SomethingElse",
},
[]*endpoint.Endpoint{},
false,
},
} { } {
t.Run(tc.title, func(t *testing.T) { t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client // Create a Kubernetes testing client
@ -171,6 +229,7 @@ func testNodeSourceEndpoints(t *testing.T) {
// Create our object under test and get the endpoints. // Create our object under test and get the endpoints.
client, err := NewNodeSource( client, err := NewNodeSource(
kubernetes, kubernetes,
tc.annotationFilter,
tc.fqdnTemplate, tc.fqdnTemplate,
) )
require.NoError(t, err) require.NoError(t, err)

View File

@ -158,7 +158,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewNodeSource(client, cfg.FQDNTemplate) return NewNodeSource(client, cfg.AnnotationFilter, cfg.FQDNTemplate)
case "service": case "service":
client, err := p.KubeClient() client, err := p.KubeClient()
if err != nil { if err != nil {