feat(services): implement Kubernetes services source

This commit is contained in:
Martin Linkhorst 2017-02-22 17:50:57 +01:00
parent 786055f3b3
commit 84910c4844
No known key found for this signature in database
GPG Key ID: 032A0944BB83C4B9
3 changed files with 270 additions and 0 deletions

6
endpoint/endpoint.go Normal file
View File

@ -0,0 +1,6 @@
package endpoint
type Endpoint struct {
DNSName string
Target string
}

63
source/service.go Normal file
View File

@ -0,0 +1,63 @@
package source
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/pkg/api/v1"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
const (
// The annotation used for figuring out which controller is responsible
controllerAnnotationKey = "external-dns.kubernetes.io/controller"
// The annotation used for defining the desired hostname
hostnameAnnotationKey = "external-dns.kubernetes.io/hostname"
// The value of the controller annotation so that we feel resposible
controllerAnnotationValue = "dns-controller"
)
// ServiceSource is an implementation of Source for Kubernetes service objects.
// It will find all services that are under our jurisdiction, i.e. annotated
// desired hostname and matching or no controller annotation. For each of the
// matches services' external entrypoints it will return a corresponding
// Endpoint object.
type ServiceSource struct {
Client kubernetes.Interface
}
// Endpoints returns endpoint objects for each service that should be processed.
func (sc *ServiceSource) Endpoints() ([]endpoint.Endpoint, error) {
services, err := sc.Client.Core().Services(v1.NamespaceAll).List(v1.ListOptions{})
if err != nil {
return nil, err
}
endpoints := []endpoint.Endpoint{}
for _, svc := range services.Items {
// Check controller annotation to see if we are responsible.
controller, exists := svc.Annotations[controllerAnnotationKey]
if exists && controller != controllerAnnotationValue {
continue
}
// Get the desired hostname of the service from the annotation.
hostname, exists := svc.Annotations[hostnameAnnotationKey]
if !exists {
continue
}
// Create an endpoint matching the desired hostname.
endpoint := endpoint.Endpoint{
DNSName: hostname,
}
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
endpoint.Target = lb.IP
endpoints = append(endpoints, endpoint)
}
}
return endpoints, nil
}

201
source/service_test.go Normal file
View File

@ -0,0 +1,201 @@
package source
import (
"testing"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/pkg/api/v1"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
// TestEndpoints tests that various services generate the correct endpoints.
func TestEndpoints(t *testing.T) {
for _, tc := range []struct {
namespace string
name string
annotations map[string]string
lbs []string
expected []endpoint.Endpoint
}{
// Completely opted-out: no endpoints returned.
{
"testing",
"foo",
map[string]string{},
[]string{"1.2.3.4"},
[]endpoint.Endpoint{},
},
// Opt-in by setting desired hostname.
{
"testing",
"foo",
map[string]string{
"external-dns.kubernetes.io/hostname": "foo.example.org",
},
[]string{"1.2.3.4"},
[]endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
},
},
// Opt-in by setting desired hostname and this controller.
{
"testing",
"foo",
map[string]string{
"external-dns.kubernetes.io/controller": "dns-controller",
"external-dns.kubernetes.io/hostname": "foo.example.org",
},
[]string{"1.2.3.4"},
[]endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
},
},
// Opt-out by setting a different controller.
{
"testing",
"foo",
map[string]string{
"external-dns.kubernetes.io/controller": "some-other-tool",
"external-dns.kubernetes.io/hostname": "foo.example.org",
},
[]string{"1.2.3.4"},
[]endpoint.Endpoint{},
},
// Make sure services are found in all namespaces.
{
"other-testing",
"foo",
map[string]string{
"external-dns.kubernetes.io/hostname": "foo.example.org",
},
[]string{"1.2.3.4"},
[]endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
},
},
// No external entrypoints lead to no endpoints.
{
"testing",
"foo",
map[string]string{
"external-dns.kubernetes.io/hostname": "foo.example.org",
},
[]string{},
[]endpoint.Endpoint{},
},
// Multiple external entrypoints lead to multiple endpoints.
{
"testing",
"foo",
map[string]string{
"external-dns.kubernetes.io/hostname": "foo.example.org",
},
[]string{"1.2.3.4", "8.8.8.8"},
[]endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
{DNSName: "foo.example.org", Target: "8.8.8.8"},
},
},
} {
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()
// Create a service to test against
ingresses := []v1.LoadBalancerIngress{}
for _, lb := range tc.lbs {
ingresses = append(ingresses, v1.LoadBalancerIngress{IP: lb})
}
service := &v1.Service{
ObjectMeta: v1.ObjectMeta{
Namespace: tc.namespace,
Name: tc.name,
Annotations: tc.annotations,
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: ingresses,
},
},
}
_, err := kubernetes.Core().Services(service.Namespace).Create(service)
if err != nil {
t.Fatal(err)
}
// Create our object under test and get the endpoints.
client := &ServiceSource{
Client: kubernetes,
}
endpoints, err := client.Endpoints()
if err != nil {
t.Fatal(err)
}
// Validate returned endpoints against desired endpoints.
validateEndpoints(t, endpoints, tc.expected)
}
}
func BenchmarkEndpoints(b *testing.B) {
kubernetes := fake.NewSimpleClientset()
service := &v1.Service{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
Name: "foo",
Annotations: map[string]string{
"external-dns.kubernetes.io/hostname": "foo.example.org",
},
},
Status: v1.ServiceStatus{
LoadBalancer: v1.LoadBalancerStatus{
Ingress: []v1.LoadBalancerIngress{
{IP: "1.2.3.4"},
{IP: "8.8.8.8"},
},
},
},
}
_, err := kubernetes.Core().Services(service.Namespace).Create(service)
if err != nil {
b.Fatal(err)
}
client := &ServiceSource{
Client: kubernetes,
}
for i := 0; i < b.N; i++ {
_, err := client.Endpoints()
if err != nil {
b.Fatal(err)
}
}
}
// test helper functions
func validateEndpoints(t *testing.T, endpoints, expected []endpoint.Endpoint) {
if len(endpoints) != len(expected) {
t.Fatalf("expected %d endpoints, got %d", len(expected), len(endpoints))
}
for i := range endpoints {
validateEndpoint(t, endpoints[i], expected[i])
}
}
func validateEndpoint(t *testing.T, endpoint, expected endpoint.Endpoint) {
if endpoint.DNSName != expected.DNSName {
t.Errorf("expected %s, got %s", expected.DNSName, endpoint.DNSName)
}
if endpoint.Target != expected.Target {
t.Errorf("expected %s, got %s", expected.Target, endpoint.Target)
}
}