Repository: kubernetes-incubator/external-dns (go)

Issue:
Handle ExternalName services
As of today, services of type `ExternalName` does not seem to be supported by ExternalDNS as they don't have an endpoint attached to them, which simply ends up with a print in the debug logs: https://github.com/kubernetes-incubator/external-dns/blob/6d2b83cbf2d91866b41510ec2ad4ba7e27449898/source/service.go#L148-L151

This seems particularly weird as it is written in the helpers that `--service-type-filter` can take `ExternalName` as a value.

Pull request:
Add support for ExternalName services
Related issue: #861

Patch:
diff --git a/docs/faq.md b/docs/faq.md
index 9017738c..c4821b27 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -51,7 +51,7 @@ As stated in the README, we are currently looking for stable maintainers for tho
 
 ### Which Kubernetes objects are supported?
 
-Services exposed via `type=LoadBalancer` and for the hostnames defined in Ingress objects as well as headless hostPort services. An initial effort to support type `NodePort` was started as of May 2018 and it is in progress at the time of writing.
+Services exposed via `type=LoadBalancer`, `type=ExternalName` and for the hostnames defined in Ingress objects as well as headless hostPort services. An initial effort to support type `NodePort` was started as of May 2018 and it is in progress at the time of writing.
 
 ### How do I specify a DNS name for my Kubernetes objects?
 
@@ -161,13 +161,13 @@ CNAMEs cannot co-exist with other records, therefore you can use the `--txt-pref
 
 You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group's scope.
 
-### What metrics can I get from ExternalDNS and what do they mean?  
+### What metrics can I get from ExternalDNS and what do they mean?
 
 ExternalDNS exposes 2 types of metrics: Sources and Registry errors.
 
 `Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc.
 
-`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state.  
+`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state.
 
 In case of an increased error count, you could correlate them with the `http_request_duration_seconds{handler="instrumented_http"}` metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down).
 
diff --git a/docs/tutorials/externalname.md b/docs/tutorials/externalname.md
new file mode 100644
index 00000000..6f9e724f
--- /dev/null
+++ b/docs/tutorials/externalname.md
@@ -0,0 +1,76 @@
+# Setting up ExternalDNS for ExternalName Services
+
+This tutorial describes how to setup ExternalDNS for usage in conjunction with an ExternalName service.
+
+## Usecases
+
+The main use cases that inspired this feature is the necessity for having a subdomain pointing to an external domain. In this scenario, it makes sense for the subdomain to have a CNAME record pointing to the external domain.
+
+## Setup
+
+### External DNS
+```yaml
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: external-dns
+spec:
+  strategy:
+    type: Recreate
+  template:
+    spec:
+      containers:
+      - name: external-dns
+        image: registry.opensource.zalan.do/teapot/external-dns:latest
+        args:
+        - --debug
+        - --source=service
+        - --source=ingress
+        - --namespace=dev
+        - --domain-filter=example.org.
+        - --provider=aws
+        - --registry=txt
+        - --txt-owner-id=dev.example.org
+```
+
+### ExternalName Service
+
+```yaml
+kind: Service
+apiVersion: v1
+metadata:
+  name: aws-service
+  annotations:
+    external-dns.alpha.kubernetes.io/hostname: tenant1.example.org,tenant2.example.org
+spec:
+  type: ExternalName
+  externalName: aws.external.com
+```
+
+This will create 2 CNAME records pointing to `aws.example.org`:
+```
+tenant1.example.org
+tenant2.example.org
+```
+
+### ExternalName Service with an IP address
+
+If `externalName` is an IP address, External DNS will create A records instead of CNAME.
+
+```yaml
+kind: Service
+apiVersion: v1
+metadata:
+  name: aws-service
+  annotations:
+    external-dns.alpha.kubernetes.io/hostname: tenant1.example.org,tenant2.example.org
+spec:
+  type: ExternalName
+  externalName: 111.111.111.111
+```
+
+This will create 2 A records pointing to `111.111.111.111`:
+```
+tenant1.example.org
+tenant2.example.org
+```
diff --git a/source/service.go b/source/service.go
index c4737f62..1fba3d62 100644
--- a/source/service.go
+++ b/source/service.go
@@ -404,6 +404,8 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro
 			return endpoints
 		}
 		endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, targets, hostname, ttl)...)
+	case v1.ServiceTypeExternalName:
+		targets = append(targets, extractServiceExternalName(svc)...)
 	}
 
 	for _, t := range targets {
@@ -432,6 +434,10 @@ func extractServiceIps(svc *v1.Service) endpoint.Targets {
 	return endpoint.Targets{svc.Spec.ClusterIP}
 }
 
+func extractServiceExternalName(svc *v1.Service) endpoint.Targets {
+	return endpoint.Targets{svc.Spec.ExternalName}
+}
+
 func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets {
 	var targets endpoint.Targets
 


Test patch:
diff --git a/source/service_test.go b/source/service_test.go
index 9947391b..b5265e33 100644
--- a/source/service_test.go
+++ b/source/service_test.go
@@ -2072,6 +2072,110 @@ func TestHeadlessServicesHostIP(t *testing.T) {
 	}
 }
 
+// TestExternalServices tests that external services generate the correct endpoints.
+func TestExternalServices(t *testing.T) {
+	for _, tc := range []struct {
+		title                    string
+		targetNamespace          string
+		svcNamespace             string
+		svcName                  string
+		svcType                  v1.ServiceType
+		compatibility            string
+		fqdnTemplate             string
+		ignoreHostnameAnnotation bool
+		labels                   map[string]string
+		annotations              map[string]string
+		externalName             string
+		expected                 []*endpoint.Endpoint
+		expectError              bool
+	}{
+		{
+			"external services return an A endpoint for the external name that is an IP address",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeExternalName,
+			"",
+			"",
+			false,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				hostnameAnnotationKey: "service.example.org",
+			},
+			"111.111.111.111",
+			[]*endpoint.Endpoint{
+				{DNSName: "service.example.org", Targets: endpoint.Targets{"111.111.111.111"}, RecordType: endpoint.RecordTypeA},
+			},
+			false,
+		},
+		{
+			"external services return a CNAME endpoint for the external name that is a domain",
+			"",
+			"testing",
+			"foo",
+			v1.ServiceTypeExternalName,
+			"",
+			"",
+			false,
+			map[string]string{"component": "foo"},
+			map[string]string{
+				hostnameAnnotationKey: "service.example.org",
+			},
+			"remote.example.com",
+			[]*endpoint.Endpoint{
+				{DNSName: "service.example.org", Targets: endpoint.Targets{"remote.example.com"}, RecordType: endpoint.RecordTypeCNAME},
+			},
+			false,
+		},
+	} {
+		t.Run(tc.title, func(t *testing.T) {
+			// Create a Kubernetes testing client
+			kubernetes := fake.NewSimpleClientset()
+
+			service := &v1.Service{
+				Spec: v1.ServiceSpec{
+					Type:         tc.svcType,
+					ExternalName: tc.externalName,
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace:   tc.svcNamespace,
+					Name:        tc.svcName,
+					Labels:      tc.labels,
+					Annotations: tc.annotations,
+				},
+				Status: v1.ServiceStatus{},
+			}
+			_, err := kubernetes.CoreV1().Services(service.Namespace).Create(service)
+			require.NoError(t, err)
+
+			// Create our object under test and get the endpoints.
+			client, _ := NewServiceSource(
+				kubernetes,
+				tc.targetNamespace,
+				"",
+				tc.fqdnTemplate,
+				false,
+				tc.compatibility,
+				true,
+				false,
+				[]string{},
+				tc.ignoreHostnameAnnotation,
+			)
+			require.NoError(t, err)
+
+			endpoints, err := client.Endpoints()
+			if tc.expectError {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+
+			// Validate returned endpoints against desired endpoints.
+			validateEndpoints(t, endpoints, tc.expected)
+		})
+	}
+}
+
 func BenchmarkServiceEndpoints(b *testing.B) {
 	kubernetes := fake.NewSimpleClientset()