From 3d97fe77a768c22d081e3d6bd33c700ebf99affd Mon Sep 17 00:00:00 2001 From: Alfred Krohmer Date: Sun, 28 Apr 2019 10:04:39 +0200 Subject: [PATCH 1/5] Headless service: retrieve endpoints via Endpoints resource; evaluate spec.publishNotReadyAddresses Currently, the endpoints of headless services are retrieved by querying pods using the pod selector of the service. Instead, we now query for an Endpoints resource with the same name as the Service object to get the endpoints for the service. This is needed in order to support the spec.publishNotReadyPods attribute of a service. --- source/service.go | 71 ++++++++++++----- source/service_test.go | 174 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 205 insertions(+), 40 deletions(-) diff --git a/source/service.go b/source/service.go index 4a70b4c39f..dc4b4b614f 100644 --- a/source/service.go +++ b/source/service.go @@ -61,6 +61,7 @@ type serviceSource struct { publishInternal bool publishHostIP bool serviceInformer coreinformers.ServiceInformer + endpointsInformer coreinformers.EndpointsInformer podInformer coreinformers.PodInformer nodeInformer coreinformers.NodeInformer serviceTypeFilter map[string]struct{} @@ -85,6 +86,7 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt // Set resync period to 0, to prevent processing when nothing has changed informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) serviceInformer := informerFactory.Core().V1().Services() + endpointsInformer := informerFactory.Core().V1().Endpoints() podInformer := informerFactory.Core().V1().Pods() nodeInformer := informerFactory.Core().V1().Nodes() @@ -96,6 +98,13 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt }, }, ) + endpointsInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + log.Debug("endpoints added") + }, + }, + ) podInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { @@ -140,6 +149,7 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt publishInternal: publishInternal, publishHostIP: publishHostIP, serviceInformer: serviceInformer, + endpointsInformer: endpointsInformer, podInformer: podInformer, nodeInformer: nodeInformer, serviceTypeFilter: serviceTypes, @@ -223,6 +233,12 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri return nil } + endpointsObject, err := sc.endpointsInformer.Lister().Endpoints(svc.Namespace).Get(svc.GetName()) + if err != nil { + log.Errorf("Get endpoints of service[%s] error:%v", svc.GetName(), err) + return endpoints + } + pods, err := sc.podInformer.Lister().Pods(svc.Namespace).List(selector) if err != nil { log.Errorf("List Pods of service[%s] error:%v", svc.GetName(), err) @@ -230,32 +246,47 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri } targetsByHeadlessDomain := make(map[string][]string) - for _, v := range pods { - headlessDomains := []string{hostname} - - if v.Spec.Hostname != "" { - headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", v.Spec.Hostname, hostname)) + for _, subset := range endpointsObject.Subsets { + addresses := subset.Addresses + if svc.Spec.PublishNotReadyAddresses { + addresses = append(addresses, subset.NotReadyAddresses...) } - for _, headlessDomain := range headlessDomains { - if sc.publishHostIP == true { - log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, v.Status.HostIP) - // To reduce traffice on the DNS API only add record for running Pods. Good Idea? - if v.Status.Phase == v1.PodRunning { - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.HostIP) - } else { - log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) + + for _, address := range addresses { + // find pod for this address + if address.TargetRef.APIVersion != "" || address.TargetRef.Kind != "Pod" { + log.Debugf("Skipping address because its target is not a pod: %v", address) + continue + } + var pod *v1.Pod + for _, v := range pods { + if v.Name == address.TargetRef.Name { + pod = v + break } - } else { - log.Debugf("Generating matching endpoint %s with PodIP %s", headlessDomain, v.Status.PodIP) - // To reduce traffice on the DNS API only add record for running Pods. Good Idea? - if v.Status.Phase == v1.PodRunning { - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.PodIP) + } + if pod == nil { + log.Errorf("Pod %s not found for address %v", address.TargetRef.Name, address) + continue + } + + headlessDomains := []string{hostname} + if pod.Spec.Hostname != "" { + headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Spec.Hostname, hostname)) + } + + for _, headlessDomain := range headlessDomains { + var ep string + if sc.publishHostIP == true { + ep = pod.Status.HostIP + log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, ep) } else { - log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) + ep = address.IP + log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, ep) } + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], ep) } } - } headlessDomains := []string{} diff --git a/source/service_test.go b/source/service_test.go index a83d1fd334..59638ccf37 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -1623,7 +1623,8 @@ func TestHeadlessServices(t *testing.T) { lbs []string podnames []string hostnames []string - phases []v1.PodPhase + podsReady []bool + publishNotReadyAddresses bool expected []*endpoint.Endpoint expectError bool }{ @@ -1648,7 +1649,8 @@ func TestHeadlessServices(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, @@ -1677,7 +1679,8 @@ func TestHeadlessServices(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{}, false, }, @@ -1703,7 +1706,8 @@ func TestHeadlessServices(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, @@ -1732,13 +1736,44 @@ func TestHeadlessServices(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodFailed}, + []bool{true, false}, + false, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, }, false, }, + { + "annotated Headless services return endpoints for all Pod if publishNotReadyAddresses is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + v1.ClusterIPNone, + []string{"1.1.1.1", "1.1.1.2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, false}, + true, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + }, + false, + }, { "annotated Headless services return endpoints for pods missing hostname", "", @@ -1760,7 +1795,8 @@ func TestHeadlessServices(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"", ""}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{ {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, @@ -1773,9 +1809,10 @@ func TestHeadlessServices(t *testing.T) { service := &v1.Service{ Spec: v1.ServiceSpec{ - Type: tc.svcType, - ClusterIP: tc.clusterIP, - Selector: tc.selector, + Type: tc.svcType, + ClusterIP: tc.clusterIP, + Selector: tc.selector, + PublishNotReadyAddresses: tc.publishNotReadyAddresses, }, ObjectMeta: metav1.ObjectMeta{ Namespace: tc.svcNamespace, @@ -1788,6 +1825,8 @@ func TestHeadlessServices(t *testing.T) { _, err := kubernetes.CoreV1().Services(service.Namespace).Create(service) require.NoError(t, err) + var addresses []v1.EndpointAddress + var notReadyAddresses []v1.EndpointAddress for i, podname := range tc.podnames { pod := &v1.Pod{ Spec: v1.PodSpec{ @@ -1802,13 +1841,41 @@ func TestHeadlessServices(t *testing.T) { }, Status: v1.PodStatus{ PodIP: tc.podIPs[i], - Phase: tc.phases[i], }, } _, err = kubernetes.CoreV1().Pods(tc.svcNamespace).Create(pod) require.NoError(t, err) + + address := v1.EndpointAddress{ + IP: tc.podIPs[i], + TargetRef: &v1.ObjectReference{ + APIVersion: "", + Kind: "Pod", + Name: podname, + }, + } + if tc.podsReady[i] { + addresses = append(addresses, address) + } else { + notReadyAddresses = append(notReadyAddresses, address) + } } + endpointsObject := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: tc.svcNamespace, + Name: tc.svcName, + Labels: tc.labels, + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: addresses, + NotReadyAddresses: notReadyAddresses, + }, + }, + } + _, err = kubernetes.CoreV1().Endpoints(tc.svcNamespace).Create(endpointsObject) + require.NoError(t, err) // Create our object under test and get the endpoints. client, _ := NewServiceSource( @@ -1857,7 +1924,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { lbs []string podnames []string hostnames []string - phases []v1.PodPhase + podsReady []bool + publishNotReadyAddresses bool expected []*endpoint.Endpoint expectError bool }{ @@ -1882,7 +1950,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, @@ -1911,7 +1980,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{}, false, }, @@ -1937,7 +2007,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, @@ -1966,13 +2037,44 @@ func TestHeadlessServicesHostIP(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"foo-0", "foo-1"}, - []v1.PodPhase{v1.PodRunning, v1.PodFailed}, + []bool{true, false}, + false, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, }, false, }, + { + "annotated Headless services return endpoints for all Pod if publishNotReadyAddresses is set", + "", + "testing", + "foo", + v1.ServiceTypeClusterIP, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + v1.ClusterIPNone, + []string{"1.1.1.1", "1.1.1.2"}, + map[string]string{ + "component": "foo", + }, + []string{}, + []string{"foo-0", "foo-1"}, + []string{"foo-0", "foo-1"}, + []bool{true, false}, + true, + []*endpoint.Endpoint{ + {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, + }, + false, + }, { "annotated Headless services return endpoints for pods missing hostname", "", @@ -1994,7 +2096,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { []string{}, []string{"foo-0", "foo-1"}, []string{"", ""}, - []v1.PodPhase{v1.PodRunning, v1.PodRunning}, + []bool{true, true}, + false, []*endpoint.Endpoint{ {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, @@ -2007,9 +2110,10 @@ func TestHeadlessServicesHostIP(t *testing.T) { service := &v1.Service{ Spec: v1.ServiceSpec{ - Type: tc.svcType, - ClusterIP: tc.clusterIP, - Selector: tc.selector, + Type: tc.svcType, + ClusterIP: tc.clusterIP, + Selector: tc.selector, + PublishNotReadyAddresses: tc.publishNotReadyAddresses, }, ObjectMeta: metav1.ObjectMeta{ Namespace: tc.svcNamespace, @@ -2022,6 +2126,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { _, err := kubernetes.CoreV1().Services(service.Namespace).Create(service) require.NoError(t, err) + var addresses []v1.EndpointAddress + var notReadyAddresses []v1.EndpointAddress for i, podname := range tc.podnames { pod := &v1.Pod{ Spec: v1.PodSpec{ @@ -2036,13 +2142,41 @@ func TestHeadlessServicesHostIP(t *testing.T) { }, Status: v1.PodStatus{ HostIP: tc.hostIPs[i], - Phase: tc.phases[i], }, } _, err = kubernetes.CoreV1().Pods(tc.svcNamespace).Create(pod) require.NoError(t, err) + + address := v1.EndpointAddress{ + IP: "4.3.2.1", + TargetRef: &v1.ObjectReference{ + APIVersion: "", + Kind: "Pod", + Name: podname, + }, + } + if tc.podsReady[i] { + addresses = append(addresses, address) + } else { + notReadyAddresses = append(notReadyAddresses, address) + } } + endpointsObject := &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: tc.svcNamespace, + Name: tc.svcName, + Labels: tc.labels, + }, + Subsets: []v1.EndpointSubset{ + { + Addresses: addresses, + NotReadyAddresses: notReadyAddresses, + }, + }, + } + _, err = kubernetes.CoreV1().Endpoints(tc.svcNamespace).Create(endpointsObject) + require.NoError(t, err) // Create our object under test and get the endpoints. client, _ := NewServiceSource( From 9228450a3e9b428ee03fd0776a601f63332931c8 Mon Sep 17 00:00:00 2001 From: Alfred Krohmer Date: Fri, 23 Aug 2019 13:05:13 +0200 Subject: [PATCH 2/5] Add CLI option to always publish not ready addresses of headless services --- main.go | 39 +++++++++++----------- pkg/apis/externaldns/types.go | 2 ++ source/service.go | 61 +++++++++++++++++++---------------- source/service_test.go | 10 +++++- source/store.go | 41 +++++++++++------------ 5 files changed, 86 insertions(+), 67 deletions(-) diff --git a/main.go b/main.go index f0aa9b708b..0f3a834eef 100644 --- a/main.go +++ b/main.go @@ -67,25 +67,26 @@ func main() { // Create a source.Config from the flags passed by the user. sourceCfg := &source.Config{ - Namespace: cfg.Namespace, - AnnotationFilter: cfg.AnnotationFilter, - FQDNTemplate: cfg.FQDNTemplate, - CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation, - IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation, - Compatibility: cfg.Compatibility, - PublishInternal: cfg.PublishInternal, - PublishHostIP: cfg.PublishHostIP, - ConnectorServer: cfg.ConnectorSourceServer, - CRDSourceAPIVersion: cfg.CRDSourceAPIVersion, - CRDSourceKind: cfg.CRDSourceKind, - KubeConfig: cfg.KubeConfig, - KubeMaster: cfg.Master, - ServiceTypeFilter: cfg.ServiceTypeFilter, - IstioIngressGatewayServices: cfg.IstioIngressGatewayServices, - CFAPIEndpoint: cfg.CFAPIEndpoint, - CFUsername: cfg.CFUsername, - CFPassword: cfg.CFPassword, - ContourLoadBalancerService: cfg.ContourLoadBalancerService, + Namespace: cfg.Namespace, + AnnotationFilter: cfg.AnnotationFilter, + FQDNTemplate: cfg.FQDNTemplate, + CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation, + IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation, + Compatibility: cfg.Compatibility, + PublishInternal: cfg.PublishInternal, + PublishHostIP: cfg.PublishHostIP, + AlwaysPublishNotReadyAddresses: cfg.AlwaysPublishNotReadyAddresses, + ConnectorServer: cfg.ConnectorSourceServer, + CRDSourceAPIVersion: cfg.CRDSourceAPIVersion, + CRDSourceKind: cfg.CRDSourceKind, + KubeConfig: cfg.KubeConfig, + KubeMaster: cfg.Master, + ServiceTypeFilter: cfg.ServiceTypeFilter, + IstioIngressGatewayServices: cfg.IstioIngressGatewayServices, + CFAPIEndpoint: cfg.CFAPIEndpoint, + CFUsername: cfg.CFUsername, + CFPassword: cfg.CFPassword, + ContourLoadBalancerService: cfg.ContourLoadBalancerService, } // Lookup all the selected sources by names and pass them the desired configuration. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 7caf6214c2..e878da0479 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -51,6 +51,7 @@ type Config struct { Compatibility string PublishInternal bool PublishHostIP bool + AlwaysPublishNotReadyAddresses bool ConnectorSourceServer string Provider string GoogleProject string @@ -286,6 +287,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule") app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal) app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP) + app.Flag("always-publish-not-ready-address", "Always publish also not ready addresses for headless services (optional)").BoolVar(&cfg.AlwaysPublishNotReadyAddresses) app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer) app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion) app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind) diff --git a/source/service.go b/source/service.go index dc4b4b614f..3ce6a11ff8 100644 --- a/source/service.go +++ b/source/service.go @@ -53,22 +53,27 @@ type serviceSource struct { client kubernetes.Interface namespace string annotationFilter string + // process Services with legacy annotations - compatibility string - fqdnTemplate *template.Template - combineFQDNAnnotation bool - ignoreHostnameAnnotation bool - publishInternal bool - publishHostIP bool - serviceInformer coreinformers.ServiceInformer - endpointsInformer coreinformers.EndpointsInformer - podInformer coreinformers.PodInformer - nodeInformer coreinformers.NodeInformer - serviceTypeFilter map[string]struct{} + compatibility string + + fqdnTemplate *template.Template + + combineFQDNAnnotation bool + ignoreHostnameAnnotation bool + publishInternal bool + publishHostIP bool + alwaysPublishNotReadyAddresses bool + + serviceInformer coreinformers.ServiceInformer + endpointsInformer coreinformers.EndpointsInformer + podInformer coreinformers.PodInformer + nodeInformer coreinformers.NodeInformer + serviceTypeFilter map[string]struct{} } // NewServiceSource creates a new serviceSource with the given config. -func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool) (Source, error) { +func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, alwaysPublishNotReadyAddresses bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool) (Source, error) { var ( tmpl *template.Template err error @@ -139,20 +144,21 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt } return &serviceSource{ - client: kubeClient, - namespace: namespace, - annotationFilter: annotationFilter, - compatibility: compatibility, - fqdnTemplate: tmpl, - combineFQDNAnnotation: combineFqdnAnnotation, - ignoreHostnameAnnotation: ignoreHostnameAnnotation, - publishInternal: publishInternal, - publishHostIP: publishHostIP, - serviceInformer: serviceInformer, - endpointsInformer: endpointsInformer, - podInformer: podInformer, - nodeInformer: nodeInformer, - serviceTypeFilter: serviceTypes, + client: kubeClient, + namespace: namespace, + annotationFilter: annotationFilter, + compatibility: compatibility, + fqdnTemplate: tmpl, + combineFQDNAnnotation: combineFqdnAnnotation, + ignoreHostnameAnnotation: ignoreHostnameAnnotation, + publishInternal: publishInternal, + publishHostIP: publishHostIP, + alwaysPublishNotReadyAddresses: alwaysPublishNotReadyAddresses, + serviceInformer: serviceInformer, + endpointsInformer: endpointsInformer, + podInformer: podInformer, + nodeInformer: nodeInformer, + serviceTypeFilter: serviceTypes, }, nil } @@ -221,6 +227,7 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) { return endpoints, nil } +// extractHeadlessEndpoints extracts endpoints from a headless service using the "Endpoints" Kubernetes API resource func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint @@ -248,7 +255,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri targetsByHeadlessDomain := make(map[string][]string) for _, subset := range endpointsObject.Subsets { addresses := subset.Addresses - if svc.Spec.PublishNotReadyAddresses { + if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses { addresses = append(addresses, subset.NotReadyAddresses...) } diff --git a/source/service_test.go b/source/service_test.go index 59638ccf37..c5549a11b2 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -53,6 +53,7 @@ func (suite *ServiceSuite) SetupTest() { "", false, false, + false, []string{}, false, ) @@ -145,6 +146,7 @@ func testServiceSourceNewServiceSource(t *testing.T) { "", false, false, + false, ti.serviceTypesFilter, false, ) @@ -1085,6 +1087,7 @@ func testServiceSourceEndpoints(t *testing.T) { tc.compatibility, false, false, + false, tc.serviceTypesFilter, tc.ignoreHostnameAnnotation, ) @@ -1255,6 +1258,7 @@ func TestClusterIpServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -1586,6 +1590,7 @@ func TestNodePortServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -1887,6 +1892,7 @@ func TestHeadlessServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -2188,6 +2194,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { tc.compatibility, true, true, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -2292,6 +2299,7 @@ func TestExternalServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -2334,7 +2342,7 @@ func BenchmarkServiceEndpoints(b *testing.B) { _, err := kubernetes.CoreV1().Services(service.Namespace).Create(service) require.NoError(b, err) - client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false, false, []string{}, false) + client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false, false, false, []string{}, false) require.NoError(b, err) for i := 0; i < b.N; i++ { diff --git a/source/store.go b/source/store.go index e42ec5ba1b..332c6b3b9f 100644 --- a/source/store.go +++ b/source/store.go @@ -41,25 +41,26 @@ var ErrSourceNotFound = errors.New("source not found") // Config holds shared configuration options for all Sources. type Config struct { - Namespace string - AnnotationFilter string - FQDNTemplate string - CombineFQDNAndAnnotation bool - IgnoreHostnameAnnotation bool - Compatibility string - PublishInternal bool - PublishHostIP bool - ConnectorServer string - CRDSourceAPIVersion string - CRDSourceKind string - KubeConfig string - KubeMaster string - ServiceTypeFilter []string - IstioIngressGatewayServices []string - CFAPIEndpoint string - CFUsername string - CFPassword string - ContourLoadBalancerService string + Namespace string + AnnotationFilter string + FQDNTemplate string + CombineFQDNAndAnnotation bool + IgnoreHostnameAnnotation bool + Compatibility string + PublishInternal bool + PublishHostIP bool + AlwaysPublishNotReadyAddresses bool + ConnectorServer string + CRDSourceAPIVersion string + CRDSourceKind string + KubeConfig string + KubeMaster string + ServiceTypeFilter []string + IstioIngressGatewayServices []string + CFAPIEndpoint string + CFUsername string + CFPassword string + ContourLoadBalancerService string } // ClientGenerator provides clients @@ -165,7 +166,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err if err != nil { return nil, err } - return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation) + return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation) case "ingress": client, err := p.KubeClient() if err != nil { From bdf71ac2b2e97f6c800e8fa004a351ffc539db6a Mon Sep 17 00:00:00 2001 From: Alfred Krohmer Date: Tue, 17 Sep 2019 10:57:50 +0200 Subject: [PATCH 3/5] Fix typo --- pkg/apis/externaldns/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index e878da0479..f2e659edae 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -287,7 +287,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule") app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal) app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP) - app.Flag("always-publish-not-ready-address", "Always publish also not ready addresses for headless services (optional)").BoolVar(&cfg.AlwaysPublishNotReadyAddresses) + app.Flag("always-publish-not-ready-addresses", "Always publish also not ready addresses for headless services (optional)").BoolVar(&cfg.AlwaysPublishNotReadyAddresses) app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer) app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion) app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind) From ffd1534423e26e7f4d63d6bd744956d1bfb147e4 Mon Sep 17 00:00:00 2001 From: Alfred Krohmer Date: Sun, 24 Nov 2019 09:16:59 +0100 Subject: [PATCH 4/5] Update documentation to include RBAC permissions for endpoints resources --- docs/tutorials/alibabacloud.md | 2 +- docs/tutorials/aws-sd.md | 2 +- docs/tutorials/aws.md | 2 +- docs/tutorials/azure-private-dns.md | 4 ++-- docs/tutorials/azure.md | 4 ++-- docs/tutorials/cloudflare.md | 2 +- docs/tutorials/contour.md | 2 +- docs/tutorials/coredns.md | 2 +- docs/tutorials/designate.md | 2 +- docs/tutorials/digitalocean.md | 2 +- docs/tutorials/dnsimple.md | 2 +- docs/tutorials/exoscale.md | 2 +- docs/tutorials/gke.md | 2 +- docs/tutorials/hostport.md | 2 +- docs/tutorials/infoblox.md | 2 +- docs/tutorials/istio.md | 2 +- docs/tutorials/linode.md | 2 +- docs/tutorials/nginx-ingress.md | 2 +- docs/tutorials/ns1.md | 2 +- docs/tutorials/oracle.md | 2 +- docs/tutorials/pdns.md | 2 +- docs/tutorials/rcodezero.md | 2 +- docs/tutorials/rdns.md | 2 +- docs/tutorials/transip.md | 2 +- docs/tutorials/vinyldns.md | 2 +- 25 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md index 5ecd087abe..fcabe6d381 100644 --- a/docs/tutorials/alibabacloud.md +++ b/docs/tutorials/alibabacloud.md @@ -144,7 +144,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/aws-sd.md b/docs/tutorials/aws-sd.md index 8e6525d1a6..56c17f3f58 100644 --- a/docs/tutorials/aws-sd.md +++ b/docs/tutorials/aws-sd.md @@ -103,7 +103,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index 608b9851da..e11610c1ba 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -113,7 +113,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md index 5e7bf917d1..a230140804 100644 --- a/docs/tutorials/azure-private-dns.md +++ b/docs/tutorials/azure-private-dns.md @@ -155,7 +155,7 @@ metadata: name: externaldns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] @@ -229,7 +229,7 @@ metadata: name: externaldns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index 81eb236bdb..10ba3667f2 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -218,7 +218,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] @@ -292,7 +292,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index bd40573170..58e3dc8f53 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -72,7 +72,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/contour.md b/docs/tutorials/contour.md index 1894bfc28a..2dadb5af99 100644 --- a/docs/tutorials/contour.md +++ b/docs/tutorials/contour.md @@ -45,7 +45,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md index de8b16fca3..06bd84bbe5 100644 --- a/docs/tutorials/coredns.md +++ b/docs/tutorials/coredns.md @@ -128,7 +128,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/designate.md b/docs/tutorials/designate.md index 5ffe560c64..8d1f5543ca 100644 --- a/docs/tutorials/designate.md +++ b/docs/tutorials/designate.md @@ -90,7 +90,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/digitalocean.md b/docs/tutorials/digitalocean.md index db0fd1348d..9c4784490f 100644 --- a/docs/tutorials/digitalocean.md +++ b/docs/tutorials/digitalocean.md @@ -66,7 +66,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/dnsimple.md b/docs/tutorials/dnsimple.md index 80b038b642..a5ce4a1435 100644 --- a/docs/tutorials/dnsimple.md +++ b/docs/tutorials/dnsimple.md @@ -57,7 +57,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/exoscale.md b/docs/tutorials/exoscale.md index cc00178890..83461b5423 100644 --- a/docs/tutorials/exoscale.md +++ b/docs/tutorials/exoscale.md @@ -69,7 +69,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/gke.md b/docs/tutorials/gke.md index f6286bf26e..70a4405be5 100644 --- a/docs/tutorials/gke.md +++ b/docs/tutorials/gke.md @@ -113,7 +113,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md index f64cbbd787..3487c5f8a6 100644 --- a/docs/tutorials/hostport.md +++ b/docs/tutorials/hostport.md @@ -50,7 +50,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/infoblox.md b/docs/tutorials/infoblox.md index b61d2045ed..8e71f0bafc 100644 --- a/docs/tutorials/infoblox.md +++ b/docs/tutorials/infoblox.md @@ -106,7 +106,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/istio.md b/docs/tutorials/istio.md index 269c10bcb3..d7f5e5b856 100644 --- a/docs/tutorials/istio.md +++ b/docs/tutorials/istio.md @@ -47,7 +47,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/linode.md b/docs/tutorials/linode.md index ad74c871ba..5429619342 100644 --- a/docs/tutorials/linode.md +++ b/docs/tutorials/linode.md @@ -62,7 +62,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/nginx-ingress.md b/docs/tutorials/nginx-ingress.md index 119348cbaa..be17bc8a01 100644 --- a/docs/tutorials/nginx-ingress.md +++ b/docs/tutorials/nginx-ingress.md @@ -213,7 +213,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/ns1.md b/docs/tutorials/ns1.md index 607cf82214..3fefed2114 100644 --- a/docs/tutorials/ns1.md +++ b/docs/tutorials/ns1.md @@ -82,7 +82,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md index 9fb6de42a6..93d544ea20 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -51,7 +51,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/pdns.md b/docs/tutorials/pdns.md index de632e0a69..73e1b1cdca 100644 --- a/docs/tutorials/pdns.md +++ b/docs/tutorials/pdns.md @@ -73,7 +73,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/rcodezero.md b/docs/tutorials/rcodezero.md index c408a96baa..3649adc0a9 100644 --- a/docs/tutorials/rcodezero.md +++ b/docs/tutorials/rcodezero.md @@ -77,7 +77,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/rdns.md b/docs/tutorials/rdns.md index 09613f5d9e..2f5d27ac3e 100644 --- a/docs/tutorials/rdns.md +++ b/docs/tutorials/rdns.md @@ -76,7 +76,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/transip.md b/docs/tutorials/transip.md index 8287baebbe..332c8d70e7 100644 --- a/docs/tutorials/transip.md +++ b/docs/tutorials/transip.md @@ -64,7 +64,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/vinyldns.md b/docs/tutorials/vinyldns.md index d8b6df8650..4ab4fffc07 100644 --- a/docs/tutorials/vinyldns.md +++ b/docs/tutorials/vinyldns.md @@ -87,7 +87,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] From 8882900851b38b5cabeda7630b18de39efec05cc Mon Sep 17 00:00:00 2001 From: Alfred Krohmer Date: Fri, 13 Dec 2019 17:13:20 +0100 Subject: [PATCH 5/5] Add possibility to specify external-dns.alpha.kubernetes.io/target annotation on pod for headless service --- source/service.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/source/service.go b/source/service.go index 3ce6a11ff8..7dd2ccb8ce 100644 --- a/source/service.go +++ b/source/service.go @@ -282,16 +282,23 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Spec.Hostname, hostname)) } + targets := getTargetsFromTargetAnnotation(pod.Annotations) + for _, headlessDomain := range headlessDomains { var ep string - if sc.publishHostIP == true { - ep = pod.Status.HostIP - log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, ep) + if len(targets) > 0 { + log.Debugf("Generating matching endpoint %s with pod target annotation %s", headlessDomain, targets) } else { - ep = address.IP - log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, ep) + if sc.publishHostIP == true { + ep = pod.Status.HostIP + log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, ep) + } else { + ep = address.IP + log.Debugf("Generating matching endpoint %s with EndpointAddress IP %s", headlessDomain, ep) + } + targets = endpoint.Targets{ep} } - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], ep) + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], targets...) } } }