diff --git a/docs/tutorials/akamai-fastdns.md b/docs/tutorials/akamai-fastdns.md index 30d67287e..6dfae56c2 100644 --- a/docs/tutorials/akamai-fastdns.md +++ b/docs/tutorials/akamai-fastdns.md @@ -95,10 +95,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] @@ -192,4 +189,4 @@ spec: **Important!**: Don't run dig, nslookup or similar immediately. You'll get hit by [negative DNS caching](https://tools.ietf.org/html/rfc2308), which is hard to flush. -Wait about 30s-1m (interval for external-dns to kick in) \ No newline at end of file +Wait about 30s-1m (interval for external-dns to kick in) diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md index fb15711ac..73d601084 100644 --- a/docs/tutorials/alibabacloud.md +++ b/docs/tutorials/alibabacloud.md @@ -147,10 +147,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/aws-sd.md b/docs/tutorials/aws-sd.md index 4343eff62..b0fa610bf 100644 --- a/docs/tutorials/aws-sd.md +++ b/docs/tutorials/aws-sd.md @@ -108,10 +108,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index 23cd0c77b..d3b814fb2 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -172,10 +172,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md index 2aefc0308..a362457e1 100644 --- a/docs/tutorials/azure-private-dns.md +++ b/docs/tutorials/azure-private-dns.md @@ -194,10 +194,7 @@ metadata: name: externaldns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] @@ -268,10 +265,7 @@ metadata: name: externaldns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] @@ -399,4 +393,4 @@ $ az network private-dns record-set a list -g externaldns -z example.com Substitute the zone for the one created above if a different domain was used. -This should show the external IP address of the service as the A record for your domain ('@' indicates the record is for the zone itself). \ No newline at end of file +This should show the external IP address of the service as the A record for your domain ('@' indicates the record is for the zone itself). diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index 29fc9fccc..0a62de70a 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -221,10 +221,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] @@ -298,10 +295,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index 442e0513b..3f2faf644 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -75,10 +75,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/contour.md b/docs/tutorials/contour.md index cd3923d59..aa9448f3a 100644 --- a/docs/tutorials/contour.md +++ b/docs/tutorials/contour.md @@ -48,10 +48,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md index 01cbd2476..1d3f64090 100644 --- a/docs/tutorials/coredns.md +++ b/docs/tutorials/coredns.md @@ -128,10 +128,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/designate.md b/docs/tutorials/designate.md index 24959447b..875eb14d9 100644 --- a/docs/tutorials/designate.md +++ b/docs/tutorials/designate.md @@ -93,7 +93,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["pods"] diff --git a/docs/tutorials/digitalocean.md b/docs/tutorials/digitalocean.md index a7f34b7af..c23fe9878 100644 --- a/docs/tutorials/digitalocean.md +++ b/docs/tutorials/digitalocean.md @@ -69,10 +69,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/dnsimple.md b/docs/tutorials/dnsimple.md index 2aca16a90..586efe940 100644 --- a/docs/tutorials/dnsimple.md +++ b/docs/tutorials/dnsimple.md @@ -60,10 +60,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/exoscale.md b/docs/tutorials/exoscale.md index 08c5905b3..64f6efbe2 100644 --- a/docs/tutorials/exoscale.md +++ b/docs/tutorials/exoscale.md @@ -72,10 +72,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/gke.md b/docs/tutorials/gke.md index cf360944a..d402ba7d5 100644 --- a/docs/tutorials/gke.md +++ b/docs/tutorials/gke.md @@ -116,10 +116,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md index 2aae14f80..7568b36b4 100644 --- a/docs/tutorials/hostport.md +++ b/docs/tutorials/hostport.md @@ -56,10 +56,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/infoblox.md b/docs/tutorials/infoblox.md index a9f51213b..edf7b9519 100644 --- a/docs/tutorials/infoblox.md +++ b/docs/tutorials/infoblox.md @@ -109,10 +109,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/istio.md b/docs/tutorials/istio.md index 5dfb32ffa..d397709af 100644 --- a/docs/tutorials/istio.md +++ b/docs/tutorials/istio.md @@ -54,10 +54,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/linode.md b/docs/tutorials/linode.md index a555b7ca6..859b87aa5 100644 --- a/docs/tutorials/linode.md +++ b/docs/tutorials/linode.md @@ -65,10 +65,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/nginx-ingress.md b/docs/tutorials/nginx-ingress.md index bd32af4c1..fd19ff324 100644 --- a/docs/tutorials/nginx-ingress.md +++ b/docs/tutorials/nginx-ingress.md @@ -222,10 +222,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/ns1.md b/docs/tutorials/ns1.md index 1511353b5..014deacef 100644 --- a/docs/tutorials/ns1.md +++ b/docs/tutorials/ns1.md @@ -85,10 +85,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md index 70092d187..98918816c 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -51,10 +51,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/pdns.md b/docs/tutorials/pdns.md index 7852fc6f7..5a555faa4 100644 --- a/docs/tutorials/pdns.md +++ b/docs/tutorials/pdns.md @@ -76,7 +76,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/rcodezero.md b/docs/tutorials/rcodezero.md index deba0bc35..4338523ba 100644 --- a/docs/tutorials/rcodezero.md +++ b/docs/tutorials/rcodezero.md @@ -80,10 +80,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/rdns.md b/docs/tutorials/rdns.md index a7e6a225a..aa7635421 100644 --- a/docs/tutorials/rdns.md +++ b/docs/tutorials/rdns.md @@ -76,10 +76,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/rfc2136.md b/docs/tutorials/rfc2136.md index d848a77a1..4c77f34fe 100644 --- a/docs/tutorials/rfc2136.md +++ b/docs/tutorials/rfc2136.md @@ -144,6 +144,8 @@ rules: - "" resources: - services + - endpoints + - pods verbs: - get - watch diff --git a/docs/tutorials/transip.md b/docs/tutorials/transip.md index d93c903a2..a416b1bc3 100644 --- a/docs/tutorials/transip.md +++ b/docs/tutorials/transip.md @@ -67,10 +67,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/docs/tutorials/vinyldns.md b/docs/tutorials/vinyldns.md index 2b19a6ca4..6c5b6fc5a 100644 --- a/docs/tutorials/vinyldns.md +++ b/docs/tutorials/vinyldns.md @@ -97,10 +97,7 @@ metadata: name: external-dns rules: - apiGroups: [""] - resources: ["services"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] + resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions"] resources: ["ingresses"] diff --git a/main.go b/main.go index 3892235ac..29e5c262a 100644 --- a/main.go +++ b/main.go @@ -71,25 +71,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 a2c33c441..e1b2f4ce7 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 @@ -295,6 +296,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-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) diff --git a/source/service.go b/source/service.go index 596b62fb0..6435066f2 100644 --- a/source/service.go +++ b/source/service.go @@ -51,22 +51,25 @@ 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 - podInformer coreinformers.PodInformer - nodeInformer coreinformers.NodeInformer - serviceTypeFilter map[string]struct{} - runner *async.BoundedFrequencyRunner + 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{} + runner *async.BoundedFrequencyRunner } // 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 @@ -84,6 +87,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() @@ -94,6 +98,12 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt }, }, ) + endpointsInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + }, + }, + ) podInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { @@ -126,19 +136,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, - 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 } @@ -207,6 +219,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 @@ -219,6 +232,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) @@ -226,32 +245,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 || sc.alwaysPublishNotReadyAddresses { + addresses = append(addresses, subset.NotReadyAddresses...) } - for _, headlessDomain := range headlessDomains { - if sc.publishHostIP { - log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, v.Status.HostIP) - // To reduce traffic 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) - } - } 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) - } 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 } } - } + 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 { + 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) + } + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], ep) + } + } } headlessDomains := []string{} diff --git a/source/service_test.go b/source/service_test.go index f687ce768..04247c1ce 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -51,6 +51,7 @@ func (suite *ServiceSuite) SetupTest() { "", false, false, + false, []string{}, false, ) @@ -143,6 +144,7 @@ func testServiceSourceNewServiceSource(t *testing.T) { "", false, false, + false, ti.serviceTypesFilter, false, ) @@ -1107,6 +1109,7 @@ func testServiceSourceEndpoints(t *testing.T) { tc.compatibility, false, false, + false, tc.serviceTypesFilter, tc.ignoreHostnameAnnotation, ) @@ -1277,6 +1280,7 @@ func TestClusterIpServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -1608,6 +1612,7 @@ func TestNodePortServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -1645,7 +1650,8 @@ func TestHeadlessServices(t *testing.T) { lbs []string podnames []string hostnames []string - phases []v1.PodPhase + podsReady []bool + publishNotReadyAddresses bool expected []*endpoint.Endpoint expectError bool }{ @@ -1670,7 +1676,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"}}, @@ -1699,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{}, false, }, @@ -1725,7 +1733,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)}, @@ -1754,13 +1763,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", "", @@ -1782,7 +1822,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"}}, }, @@ -1822,9 +1863,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, @@ -1837,6 +1879,7 @@ func TestHeadlessServices(t *testing.T) { _, err := kubernetes.CoreV1().Services(service.Namespace).Create(service) require.NoError(t, err) + var addresses, notReadyAddresses []v1.EndpointAddress for i, podname := range tc.podnames { pod := &v1.Pod{ Spec: v1.PodSpec{ @@ -1851,13 +1894,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( @@ -1869,6 +1940,7 @@ func TestHeadlessServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -1906,7 +1978,8 @@ func TestHeadlessServicesHostIP(t *testing.T) { lbs []string podnames []string hostnames []string - phases []v1.PodPhase + podsReady []bool + publishNotReadyAddresses bool expected []*endpoint.Endpoint expectError bool }{ @@ -1931,7 +2004,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"}}, @@ -1960,7 +2034,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, }, @@ -1986,7 +2061,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)}, @@ -2015,13 +2091,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", "", @@ -2043,7 +2150,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"}}, }, @@ -2056,9 +2164,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, @@ -2071,6 +2180,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{ @@ -2085,13 +2196,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( @@ -2103,6 +2242,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { tc.compatibility, true, true, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -2207,6 +2347,7 @@ func TestExternalServices(t *testing.T) { tc.compatibility, true, false, + false, []string{}, tc.ignoreHostnameAnnotation, ) @@ -2249,7 +2390,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 586068068..02db49224 100644 --- a/source/store.go +++ b/source/store.go @@ -40,25 +40,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 @@ -164,7 +165,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 {