Skip to content

Commit

Permalink
ClusterIp Service support (kubernetes-sigs#278)
Browse files Browse the repository at this point in the history
* ClusterIP service support

- First pass at addresssing kubernetes-sigs#187 by allowing services with type ClusterIP to be directly supported

* Getting existing tests to pass

* Adjusting formatting for gofmt/govet

* Adding in guard logic around publishing of ClusterIP sources

* Addressing PR feedback

* Adding in CHANGELOG entry

* Adding in Headless service test
  • Loading branch information
jrnt30 authored and hjacobs committed Aug 17, 2017
1 parent 8965720 commit 9c3a980
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ cscope.*
# coverage output
cover.out
*.coverprofile
external-dns
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
- ExternalDNS now services of type `ClusterIP` with the use of the `--publish-internal-services`. Enabling this will now create the apprioriate A records for the given service's internal ip. @jrnt30

## v0.4.3 - 2017-08-17

- Fix to have external target annotations on ingress resources replace existing endpoints instead of appending to them (#318)

## v0.4.2 - 2017-08-03

- Fix to support multiple hostnames for Molecule Software's [route53-kubernetes](https://github.com/wearemolecule/route53-kubernetes) compatibility (#301)
Expand Down
7 changes: 4 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ func main() {

// Create a source.Config from the flags passed by the user.
sourceCfg := &source.Config{
Namespace: cfg.Namespace,
FQDNTemplate: cfg.FQDNTemplate,
Compatibility: cfg.Compatibility,
Namespace: cfg.Namespace,
FQDNTemplate: cfg.FQDNTemplate,
Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
}

// Lookup all the selected sources by names and pass them the desired configuration.
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Config struct {
Namespace string
FQDNTemplate string
Compatibility string
PublishInternal bool
Provider string
GoogleProject string
DomainFilter []string
Expand All @@ -58,6 +59,7 @@ var defaultConfig = &Config{
Namespace: "",
FQDNTemplate: "",
Compatibility: "",
PublishInternal: false,
Provider: "",
GoogleProject: "",
DomainFilter: []string{},
Expand Down Expand Up @@ -95,6 +97,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional)").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate)
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)

// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "inmemory")
Expand Down
86 changes: 55 additions & 31 deletions source/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,19 @@ import (
// serviceSource is an implementation of Source for Kubernetes service objects.
// It will find all services that are under our jurisdiction, i.e. annotated
// desired hostname and matching or no controller annotation. For each of the
// matched services' external entrypoints it will return a corresponding
// matched services' entrypoints it will return a corresponding
// Endpoint object.
type serviceSource struct {
client kubernetes.Interface
namespace string
// process Services with legacy annotations
compatibility string
fqdnTemplate *template.Template
compatibility string
fqdnTemplate *template.Template
publishInternal bool
}

// NewServiceSource creates a new serviceSource with the given config.
func NewServiceSource(kubeClient kubernetes.Interface, namespace, fqdnTemplate, compatibility string) (Source, error) {
func NewServiceSource(kubeClient kubernetes.Interface, namespace, fqdnTemplate, compatibility string, publishInternal bool) (Source, error) {
var (
tmpl *template.Template
err error
Expand All @@ -60,10 +61,11 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, fqdnTemplate,
}

return &serviceSource{
client: kubeClient,
namespace: namespace,
compatibility: compatibility,
fqdnTemplate: tmpl,
client: kubeClient,
namespace: namespace,
compatibility: compatibility,
fqdnTemplate: tmpl,
publishInternal: publishInternal,
}, nil
}

Expand All @@ -85,7 +87,7 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
continue
}

svcEndpoints := endpointsFromService(&svc)
svcEndpoints := sc.endpoints(&svc)

// process legacy annotations if no endpoints were returned and compatibility mode is enabled.
if len(svcEndpoints) == 0 && sc.compatibility != "" {
Expand Down Expand Up @@ -122,21 +124,13 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End
}

hostname := buf.String()
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
//TODO(ideahitme): consider retrieving record type from resource annotation instead of empty
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, ""))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, ""))
}
}
endpoints = sc.generateEndpoints(svc, hostname)

return endpoints, nil
}

// endpointsFromService extracts the endpoints from a service object
func endpointsFromService(svc *v1.Service) []*endpoint.Endpoint {
func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint

// Get the desired hostname of the service from the annotation.
Expand All @@ -145,20 +139,50 @@ func endpointsFromService(svc *v1.Service) []*endpoint.Endpoint {
return nil
}

// splits the hostname annotation and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")

for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".")
// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
//TODO(ideahitme): consider retrieving record type from resource annotation instead of empty
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, ""))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, ""))
}
endpoints = append(endpoints, sc.generateEndpoints(svc, hostname)...)
}

return endpoints
}

func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint

hostname = strings.TrimSuffix(hostname, ".")
switch svc.Spec.Type {
case v1.ServiceTypeLoadBalancer:
endpoints = append(endpoints, extractLoadBalancerEndpoints(svc, hostname)...)
case v1.ServiceTypeClusterIP:
if sc.publishInternal {
endpoints = append(endpoints, extractServiceIps(svc, hostname)...)
}
}

return endpoints
}

func extractServiceIps(svc *v1.Service, hostname string) []*endpoint.Endpoint {
if svc.Spec.ClusterIP == v1.ClusterIPNone {
log.Debugf("Unable to associate %s headless service with a Cluster IP", svc.Name)
return []*endpoint.Endpoint{}
}

return []*endpoint.Endpoint{endpoint.NewEndpoint(hostname, svc.Spec.ClusterIP, "")}
}

func extractLoadBalancerEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint

// Create a corresponding endpoint for each configured external entrypoint.
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
//TODO(ideahitme): consider retrieving record type from resource annotation instead of empty
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.IP, ""))
}
if lb.Hostname != "" {
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, lb.Hostname, ""))
}
}

Expand Down
Loading

0 comments on commit 9c3a980

Please sign in to comment.