feat(source): support services annotated with mate's annotations (#141)

This commit is contained in:
Martin Linkhorst 2017-04-10 15:43:41 +02:00 committed by Henning Jacobs
parent 04bbdb5d80
commit 9d48d89240
6 changed files with 122 additions and 7 deletions

View File

@ -76,7 +76,7 @@ func main() {
log.Fatal(err)
}
source.Register("service", source.NewServiceSource(client, cfg.Namespace))
source.Register("service", source.NewServiceSource(client, cfg.Namespace, cfg.Compatibility))
source.Register("ingress", source.NewIngressSource(client, cfg.Namespace))
sources := source.NewMultiSource(source.LookupMultiple(cfg.Sources...)...)

View File

@ -38,6 +38,7 @@ type Config struct {
Sources []string
Provider string
GoogleProject string
Compatibility bool
MetricsAddress string
Interval time.Duration
Once bool
@ -62,6 +63,7 @@ func (cfg *Config) ParseFlags(args []string) error {
flags.StringArrayVar(&cfg.Sources, "source", nil, "the sources to gather endpoints from")
flags.StringVar(&cfg.Provider, "provider", "", "the DNS provider to materialize the records in")
flags.StringVar(&cfg.GoogleProject, "google-project", "", "gcloud project to target")
flags.BoolVar(&cfg.Compatibility, "compatibility", false, "enable to process annotation semantics from legacy implementations")
flags.StringVar(&cfg.MetricsAddress, "metrics-address", defaultMetricsAddress, "address to expose metrics on")
flags.StringVar(&cfg.LogFormat, "log-format", defaultLogFormat, "log format output. options: [\"text\", \"json\"]")
flags.DurationVar(&cfg.Interval, "interval", time.Minute, "interval between synchronizations")

View File

@ -40,6 +40,7 @@ func TestParseFlags(t *testing.T) {
Sources: nil,
Provider: "",
GoogleProject: "",
Compatibility: false,
MetricsAddress: defaultMetricsAddress,
Interval: time.Minute,
Once: false,
@ -60,6 +61,7 @@ func TestParseFlags(t *testing.T) {
Sources: nil,
Provider: "",
GoogleProject: "",
Compatibility: false,
MetricsAddress: defaultMetricsAddress,
Interval: time.Minute,
Once: false,
@ -80,6 +82,7 @@ func TestParseFlags(t *testing.T) {
Sources: nil,
Provider: "",
GoogleProject: "",
Compatibility: false,
MetricsAddress: defaultMetricsAddress,
Interval: time.Minute,
Once: false,
@ -105,6 +108,7 @@ func TestParseFlags(t *testing.T) {
Sources: nil,
Provider: "",
GoogleProject: "",
Compatibility: false,
MetricsAddress: defaultMetricsAddress,
Interval: time.Minute,
Once: false,
@ -124,6 +128,7 @@ func TestParseFlags(t *testing.T) {
"--source", "source",
"--provider", "provider",
"--google-project", "project",
"--compatibility",
"--metrics-address", "127.0.0.1:9099",
"--interval", "10m",
"--once",
@ -138,6 +143,7 @@ func TestParseFlags(t *testing.T) {
Sources: []string{"source"},
Provider: "provider",
GoogleProject: "project",
Compatibility: true,
MetricsAddress: "127.0.0.1:9099",
Interval: 10 * time.Minute,
Once: true,

57
source/compatibility.go Normal file
View File

@ -0,0 +1,57 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package source
import (
"k8s.io/client-go/pkg/api/v1"
"github.com/kubernetes-incubator/external-dns/endpoint"
)
const (
mateAnnotationKey = "zalando.org/dnsname"
)
// legacyEndpointsFromService tries to retrieve Endpoints from Services
// annotated with legacy annotations.
func legacyEndpointsFromService(svc *v1.Service) []*endpoint.Endpoint {
return legacyEndpointsFromMateService(svc)
}
// legacyEndpointsFromMateService tries to retrieve Endpoints from Services
// annotated with Mate's annotation semantics.
func legacyEndpointsFromMateService(svc *v1.Service) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
// Get the desired hostname of the service from the annotation.
hostname, exists := svc.Annotations[mateAnnotationKey]
if !exists {
return nil
}
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname))
}
}
return endpoints
}

View File

@ -31,11 +31,17 @@ import (
type serviceSource struct {
client kubernetes.Interface
namespace string
// set to true to process Services with legacy annotations
compatibility bool
}
// NewServiceSource creates a new serviceSource with the given client and namespace scope.
func NewServiceSource(client kubernetes.Interface, namespace string) Source {
return &serviceSource{client: client, namespace: namespace}
func NewServiceSource(client kubernetes.Interface, namespace string, compatibility bool) Source {
return &serviceSource{
client: client,
namespace: namespace,
compatibility: compatibility,
}
}
// Endpoints returns endpoint objects for each service that should be processed.
@ -49,6 +55,12 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
for _, svc := range services.Items {
svcEndpoints := endpointsFromService(&svc)
// process legacy annotations if no endpoints were returned and compatibility mode is enabled.
if len(svcEndpoints) == 0 && sc.compatibility {
svcEndpoints = legacyEndpointsFromService(&svc)
}
if len(svcEndpoints) != 0 {
endpoints = append(endpoints, svcEndpoints...)
}
@ -64,13 +76,13 @@ func endpointsFromService(svc *v1.Service) []*endpoint.Endpoint {
// Check controller annotation to see if we are responsible.
controller, exists := svc.Annotations[controllerAnnotationKey]
if exists && controller != controllerAnnotationValue {
return endpoints
return nil
}
// Get the desired hostname of the service from the annotation.
hostname, exists := svc.Annotations[hostnameAnnotationKey]
if !exists {
return endpoints
return nil
}
// Create a corresponding endpoint for each configured external entrypoint.

View File

@ -40,6 +40,7 @@ func testServiceEndpoints(t *testing.T) {
targetNamespace string
svcNamespace string
svcName string
compatibility bool
annotations map[string]string
lbs []string
expected []*endpoint.Endpoint
@ -49,6 +50,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{},
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{},
@ -58,6 +60,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
@ -71,6 +74,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
@ -84,6 +88,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org", // Trailing dot is omitted
},
@ -98,6 +103,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{
controllerAnnotationKey: controllerAnnotationValue,
hostnameAnnotationKey: "foo.example.org.",
@ -112,6 +118,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{
controllerAnnotationKey: "some-other-tool",
hostnameAnnotationKey: "foo.example.org.",
@ -124,6 +131,7 @@ func testServiceEndpoints(t *testing.T) {
"testing",
"testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
@ -137,6 +145,7 @@ func testServiceEndpoints(t *testing.T) {
"testing",
"other-testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
@ -148,6 +157,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"other-testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
@ -161,6 +171,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
@ -172,6 +183,7 @@ func testServiceEndpoints(t *testing.T) {
"",
"testing",
"foo",
false,
map[string]string{
hostnameAnnotationKey: "foo.example.org.",
},
@ -181,6 +193,32 @@ func testServiceEndpoints(t *testing.T) {
{DNSName: "foo.example.org", Target: "8.8.8.8"},
},
},
{
"services annotated with legacy mate annotations are ignored in default mode",
"",
"testing",
"foo",
false,
map[string]string{
"zalando.org/dnsname": "foo.example.org.",
},
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{},
},
{
"services annotated with legacy mate annotations return an endpoint in compatibility mode",
"",
"testing",
"foo",
true,
map[string]string{
"zalando.org/dnsname": "foo.example.org.",
},
[]string{"1.2.3.4"},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Target: "1.2.3.4"},
},
},
} {
t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client
@ -215,7 +253,7 @@ func testServiceEndpoints(t *testing.T) {
}
// Create our object under test and get the endpoints.
client := NewServiceSource(kubernetes, tc.targetNamespace)
client := NewServiceSource(kubernetes, tc.targetNamespace, tc.compatibility)
endpoints, err := client.Endpoints()
if err != nil {
@ -254,7 +292,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
b.Fatal(err)
}
client := NewServiceSource(kubernetes, v1.NamespaceAll)
client := NewServiceSource(kubernetes, v1.NamespaceAll, false)
for i := 0; i < b.N; i++ {
_, err := client.Endpoints()